Raw impl of task lists (parser, span, drawable)

This commit is contained in:
Dimitry Ivanov 2017-10-22 15:19:55 +03:00
parent 8aea662f50
commit f95918104b
16 changed files with 604 additions and 5 deletions

View File

@ -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-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) [![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. <u>**No WebView is required**</u>. 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). **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. <u>**No WebView is required**</u>. 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).
<sup>*</sup>*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* <sup>*</sup>*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*

View File

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

View 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>

View 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>

View File

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

View File

@ -0,0 +1,6 @@
package ru.noties.markwon.tasklist;
import org.commonmark.node.CustomBlock;
public class TaskListBlock extends CustomBlock {
}

View File

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

View File

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

View File

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

View File

@ -18,6 +18,8 @@ dependencies {
compile COMMON_MARK compile COMMON_MARK
compile COMMON_MARK_STRIKETHROUGHT compile COMMON_MARK_STRIKETHROUGHT
compile COMMON_MARK_TABLE compile COMMON_MARK_TABLE
compile project(':library-task-list')
} }
if (project.hasProperty('release')) { if (project.hasProperty('release')) {

View File

@ -15,6 +15,7 @@ import org.commonmark.parser.Parser;
import java.util.Arrays; import java.util.Arrays;
import ru.noties.markwon.renderer.SpannableRenderer; import ru.noties.markwon.renderer.SpannableRenderer;
import ru.noties.markwon.tasklist.TaskListExtension;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public abstract class Markwon { public abstract class Markwon {
@ -27,7 +28,7 @@ public abstract class Markwon {
*/ */
public static Parser createParser() { public static Parser createParser() {
return new Parser.Builder() return new Parser.Builder()
.extensions(Arrays.asList(StrikethroughExtension.create(), TablesExtension.create())) .extensions(Arrays.asList(StrikethroughExtension.create(), TablesExtension.create(), TaskListExtension.create()))
.build(); .build();
} }

View File

@ -14,6 +14,7 @@ import org.commonmark.node.AbstractVisitor;
import org.commonmark.node.BlockQuote; import org.commonmark.node.BlockQuote;
import org.commonmark.node.BulletList; import org.commonmark.node.BulletList;
import org.commonmark.node.Code; import org.commonmark.node.Code;
import org.commonmark.node.CustomBlock;
import org.commonmark.node.CustomNode; import org.commonmark.node.CustomNode;
import org.commonmark.node.Emphasis; import org.commonmark.node.Emphasis;
import org.commonmark.node.FencedCodeBlock; 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.OrderedListItemSpan;
import ru.noties.markwon.spans.StrongEmphasisSpan; import ru.noties.markwon.spans.StrongEmphasisSpan;
import ru.noties.markwon.spans.TableRowSpan; import ru.noties.markwon.spans.TableRowSpan;
import ru.noties.markwon.spans.TaskListSpan;
import ru.noties.markwon.spans.ThematicBreakSpan; import ru.noties.markwon.spans.ThematicBreakSpan;
import ru.noties.markwon.tasklist.TaskListBlock;
import ru.noties.markwon.tasklist.TaskListItem;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public class SpannableMarkdownVisitor extends AbstractVisitor { public class SpannableMarkdownVisitor extends AbstractVisitor {
@ -269,6 +273,19 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
newLine(); 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 @Override
public void visit(CustomNode customNode) { public void visit(CustomNode customNode) {
@ -279,7 +296,17 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
setSpan(length, new StrikethroughSpan()); setSpan(length, new StrikethroughSpan());
} else if (!handleTableNodes(customNode)) { } 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);
}
} }
} }

View File

@ -4,6 +4,7 @@ import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.support.annotation.AttrRes; import android.support.annotation.AttrRes;
import android.support.annotation.ColorInt; import android.support.annotation.ColorInt;
import android.support.annotation.Dimension; import android.support.annotation.Dimension;
@ -39,9 +40,13 @@ public class SpannableTheme {
} }
public static Builder builderWithDefaults(@NonNull Context context) { 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); final Dip dip = new Dip(context);
return new Builder() return new Builder()
.linkColor(resolve(context, android.R.attr.textColorLink)) .linkColor(linkColor)
.codeMultilineMargin(dip.toPx(8)) .codeMultilineMargin(dip.toPx(8))
.blockMargin(dip.toPx(24)) .blockMargin(dip.toPx(24))
.blockQuoteWidth(dip.toPx(4)) .blockQuoteWidth(dip.toPx(4))
@ -49,7 +54,8 @@ public class SpannableTheme {
.headingBreakHeight(dip.toPx(1)) .headingBreakHeight(dip.toPx(1))
.thematicBreakHeight(dip.toPx(4)) .thematicBreakHeight(dip.toPx(4))
.tableCellPadding(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) { 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 // by default paint.color * TABLE_ODD_ROW_DEF_ALPHA
protected final int tableOddRowBackgroundColor; protected final int tableOddRowBackgroundColor;
protected final Drawable taskListDrawable;
protected SpannableTheme(@NonNull Builder builder) { protected SpannableTheme(@NonNull Builder builder) {
this.linkColor = builder.linkColor; this.linkColor = builder.linkColor;
this.blockMargin = builder.blockMargin; this.blockMargin = builder.blockMargin;
@ -169,6 +177,7 @@ public class SpannableTheme {
this.tableBorderColor = builder.tableBorderColor; this.tableBorderColor = builder.tableBorderColor;
this.tableBorderWidth = builder.tableBorderWidth; this.tableBorderWidth = builder.tableBorderWidth;
this.tableOddRowBackgroundColor = builder.tableOddRowBackgroundColor; this.tableOddRowBackgroundColor = builder.tableOddRowBackgroundColor;
this.taskListDrawable = builder.taskListDrawable;
} }
@ -363,6 +372,11 @@ public class SpannableTheme {
paint.setStyle(Paint.Style.FILL); paint.setStyle(Paint.Style.FILL);
} }
@NonNull
public Drawable getTaskListDrawable() {
return taskListDrawable;
}
public static class Builder { public static class Builder {
private int linkColor; private int linkColor;
@ -386,6 +400,7 @@ public class SpannableTheme {
private int tableBorderColor; private int tableBorderColor;
private int tableBorderWidth; private int tableBorderWidth;
private int tableOddRowBackgroundColor; private int tableOddRowBackgroundColor;
private Drawable taskListDrawable;
Builder() { Builder() {
} }
@ -412,6 +427,7 @@ public class SpannableTheme {
this.tableBorderColor = theme.tableBorderColor; this.tableBorderColor = theme.tableBorderColor;
this.tableBorderWidth = theme.tableBorderWidth; this.tableBorderWidth = theme.tableBorderWidth;
this.tableOddRowBackgroundColor = theme.tableOddRowBackgroundColor; this.tableOddRowBackgroundColor = theme.tableOddRowBackgroundColor;
this.taskListDrawable = theme.taskListDrawable;
} }
public Builder linkColor(@ColorInt int linkColor) { public Builder linkColor(@ColorInt int linkColor) {
@ -519,6 +535,11 @@ public class SpannableTheme {
return this; return this;
} }
public Builder taskListDrawable(@NonNull Drawable taskListDrawable) {
this.taskListDrawable = taskListDrawable;
return this;
}
public SpannableTheme build() { public SpannableTheme build() {
return new SpannableTheme(this); return new SpannableTheme(this);
} }

View File

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

View File

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

View File

@ -1 +1 @@
include ':app', ':library', ':library-image-loader', ':library-view' include ':app', ':library', ':library-image-loader', ':library-view', ':library-task-list'