Introduced SpannableTheme

This commit is contained in:
Dimitry Ivanov 2017-05-16 17:55:33 +03:00
parent e50789bc40
commit e0c10c658b
40 changed files with 1008 additions and 710 deletions

View File

@ -18,4 +18,5 @@ dependencies {
compile project(':library-renderer')
compile 'ru.noties:debug:3.0.0@jar'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup.okhttp3:okhttp:3.8.0'
}

View File

@ -30,7 +30,7 @@ import ru.noties.debug.Debug;
import ru.noties.markwon.renderer.*;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.CodeSpan;
import ru.noties.markwon.spans.DrawableSpanUtils;
import ru.noties.markwon.spans.AsyncDrawableSpanUtils;
public class MainActivity extends Activity {
@ -101,50 +101,52 @@ public class MainActivity extends Activity {
.build();
final Node node = parser.parse(md);
final SpannableConfiguration configuration = SpannableConfiguration.builder(MainActivity.this)
.setAsyncDrawableLoader(new AsyncDrawable.Loader() {
@Override
public void load(@NonNull String destination, @NonNull final AsyncDrawable drawable) {
Debug.i(destination);
final Target target = new Target() {
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
Debug.i();
final Drawable d = new BitmapDrawable(getResources(), bitmap);
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
drawable.setResult(d);
// textView.setText(textView.getText());
}
// final SpannableConfiguration configuration = SpannableConfiguration.builder(MainActivity.this)
// .setAsyncDrawableLoader(new AsyncDrawable.Loader() {
// @Override
// public void load(@NonNull String destination, @NonNull final AsyncDrawable drawable) {
// Debug.i(destination);
// final Target target = new Target() {
// @Override
// public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
// Debug.i();
// final Drawable d = new BitmapDrawable(getResources(), bitmap);
// d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
// drawable.setResult(d);
//// textView.setText(textView.getText());
// }
//
// @Override
// public void onBitmapFailed(Drawable errorDrawable) {
// Debug.i();
// }
//
// @Override
// public void onPrepareLoad(Drawable placeHolderDrawable) {
// Debug.i();
// }
// };
// targets.add(target);
//
// picasso.load(destination)
// .tag(destination)
// .into(target);
//
// }
//
// @Override
// public void cancel(@NonNull String destination) {
// Debug.i(destination);
// picasso
// .cancelTag(destination);
// }
// })
// .setCodeConfig(CodeSpan.Config.builder().setTextSize(
// (int) (getResources().getDisplayMetrics().density * 14 + .5F)
// ).setMultilineMargin((int) (getResources().getDisplayMetrics().density * 8 + .5F)).build())
// .build();
@Override
public void onBitmapFailed(Drawable errorDrawable) {
Debug.i();
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
Debug.i();
}
};
targets.add(target);
picasso.load(destination)
.tag(destination)
.into(target);
}
@Override
public void cancel(@NonNull String destination) {
Debug.i(destination);
picasso
.cancelTag(destination);
}
})
.setCodeConfig(CodeSpan.Config.builder().setTextSize(
(int) (getResources().getDisplayMetrics().density * 14 + .5F)
).setMultilineMargin((int) (getResources().getDisplayMetrics().density * 8 + .5F)).build())
.build();
final SpannableConfiguration configuration = SpannableConfiguration.create(MainActivity.this);
final CharSequence text = new ru.noties.markwon.renderer.SpannableRenderer().render(
configuration,
@ -168,7 +170,7 @@ public class MainActivity extends Activity {
// NB! LinkMovementMethod forces frequent updates...
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setText(text);
DrawableSpanUtils.scheduleDrawables(textView);
AsyncDrawableSpanUtils.scheduleDrawables(textView);
}
});
}

View File

@ -53,8 +53,8 @@
//import ru.noties.markwon.spans.EmphasisSpan;
//import ru.noties.markwon.spans.BulletListItemSpan;
//import ru.noties.markwon.spans.StrongEmphasisSpan;
//import ru.noties.markwon.spans.SubSpan;
//import ru.noties.markwon.spans.SupSpan;
//import ru.noties.markwon.spans.SubScriptSpan;
//import ru.noties.markwon.spans.SuperScriptSpan;
//import ru.noties.markwon.spans.ThematicBreakSpan;
//
//public class SpannableRenderer implements Renderer {
@ -263,9 +263,9 @@
// 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);
// builder.setSpan(new SuperScriptSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// } else if("sub".equals(item.tag)) {
// builder.setSpan(new SubSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// builder.setSpan(new SubScriptSpan(), 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

View File

@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
<TextView
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -1,6 +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>
<color name="colorPrimary">#424242</color>
<color name="colorPrimaryDark">#212121</color>
<color name="colorAccent">#4caf50</color>
</resources>

View File

@ -3,7 +3,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'com.android.tools.build:gradle:2.3.2'
}
}

View File

@ -15,8 +15,6 @@ android {
dependencies {
compile project(':library-spans')
compile SUPPORT_ANNOTATIONS
compile COMMON_MARK
compile COMMON_MARK_STRIKETHROUGHT

View File

@ -0,0 +1,27 @@
package ru.noties.markwon.renderer;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.provider.Browser;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.View;
import ru.noties.markwon.spans.LinkSpan;
class LinkResolverDef implements LinkSpan.Resolver {
@Override
public void resolve(View view, @NonNull String link) {
final Uri uri = Uri.parse(link);
final Context context = view.getContext();
final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.w("LinkResolverDef", "Actvity was not found for intent, " + intent.toString());
}
}
}

View File

@ -4,12 +4,8 @@ import android.content.Context;
import android.support.annotation.NonNull;
import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.spans.BlockQuoteSpan;
import ru.noties.markwon.spans.BulletListItemSpan;
import ru.noties.markwon.spans.CodeSpan;
import ru.noties.markwon.spans.HeadingSpan;
import ru.noties.markwon.spans.OrderedListItemSpan;
import ru.noties.markwon.spans.ThematicBreakSpan;
import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.SpannableTheme;
public class SpannableConfiguration {
@ -22,140 +18,81 @@ public class SpannableConfiguration {
return new Builder(context);
}
private final BlockQuoteSpan.Config blockQuoteConfig;
private final CodeSpan.Config codeConfig;
private final BulletListItemSpan.Config bulletListConfig;
private final HeadingSpan.Config headingConfig;
private final ThematicBreakSpan.Config thematicConfig;
private final OrderedListItemSpan.Config orderedListConfig;
private final SpannableTheme theme;
private final AsyncDrawable.Loader asyncDrawableLoader;
private final SyntaxHighlight syntaxHighlight;
private final LinkSpan.Resolver linkResolver;
private SpannableConfiguration(Builder builder) {
this.blockQuoteConfig = builder.blockQuoteConfig;
this.codeConfig = builder.codeConfig;
this.bulletListConfig = builder.bulletListConfig;
this.headingConfig = builder.headingConfig;
this.thematicConfig = builder.thematicConfig;
this.orderedListConfig = builder.orderedListConfig;
this.theme = builder.theme;
this.asyncDrawableLoader = builder.asyncDrawableLoader;
this.syntaxHighlight = builder.syntaxHighlight;
this.linkResolver = builder.linkResolver;
}
public BlockQuoteSpan.Config getBlockQuoteConfig() {
return blockQuoteConfig;
public SpannableTheme theme() {
return theme;
}
public CodeSpan.Config getCodeConfig() {
return codeConfig;
}
public BulletListItemSpan.Config getBulletListConfig() {
return bulletListConfig;
}
public HeadingSpan.Config getHeadingConfig() {
return headingConfig;
}
public ThematicBreakSpan.Config getThematicConfig() {
return thematicConfig;
}
public OrderedListItemSpan.Config getOrderedListConfig() {
return orderedListConfig;
}
public AsyncDrawable.Loader getAsyncDrawableLoader() {
public AsyncDrawable.Loader asyncDrawableLoader() {
return asyncDrawableLoader;
}
public SyntaxHighlight syntaxHighlight() {
return syntaxHighlight;
}
public LinkSpan.Resolver linkResolver() {
return linkResolver;
}
public static class Builder {
private final Context context;
private BlockQuoteSpan.Config blockQuoteConfig;
private CodeSpan.Config codeConfig;
private BulletListItemSpan.Config bulletListConfig;
private HeadingSpan.Config headingConfig;
private ThematicBreakSpan.Config thematicConfig;
private OrderedListItemSpan.Config orderedListConfig;
private SpannableTheme theme;
private AsyncDrawable.Loader asyncDrawableLoader;
private SyntaxHighlight syntaxHighlight;
private LinkSpan.Resolver linkResolver;
public Builder(Context context) {
this.context = context;
}
public Builder setBlockQuoteConfig(@NonNull BlockQuoteSpan.Config blockQuoteConfig) {
this.blockQuoteConfig = blockQuoteConfig;
public Builder theme(SpannableTheme theme) {
this.theme = theme;
return this;
}
public Builder setCodeConfig(@NonNull CodeSpan.Config codeConfig) {
this.codeConfig = codeConfig;
return this;
}
public Builder setBulletListConfig(@NonNull BulletListItemSpan.Config bulletListConfig) {
this.bulletListConfig = bulletListConfig;
return this;
}
public Builder setHeadingConfig(@NonNull HeadingSpan.Config headingConfig) {
this.headingConfig = headingConfig;
return this;
}
public Builder setThematicConfig(@NonNull ThematicBreakSpan.Config thematicConfig) {
this.thematicConfig = thematicConfig;
return this;
}
public Builder setOrderedListConfig(@NonNull OrderedListItemSpan.Config orderedListConfig) {
this.orderedListConfig = orderedListConfig;
return this;
}
public Builder setAsyncDrawableLoader(AsyncDrawable.Loader asyncDrawableLoader) {
public Builder asyncDrawableLoader(AsyncDrawable.Loader asyncDrawableLoader) {
this.asyncDrawableLoader = asyncDrawableLoader;
return this;
}
// todo, change to something more reliable
// todo, must mention that bullet/ordered/quote must have the same margin (or maybe we can just enforce it?)
// actually it does make sense to have `blockQuote` & `list` margins (if they are different)
// todo, maybe move defaults to configuration classes?
public Builder syntaxHighlight(SyntaxHighlight syntaxHighlight) {
this.syntaxHighlight = syntaxHighlight;
return this;
}
public Builder linkResolver(LinkSpan.Resolver linkResolver) {
this.linkResolver = linkResolver;
return this;
}
public SpannableConfiguration build() {
if (blockQuoteConfig == null) {
blockQuoteConfig = new BlockQuoteSpan.Config(
px(24),
0,
0
);
}
if (codeConfig == null) {
codeConfig = CodeSpan.Config.builder()
.setMultilineMargin(px(8))
.build();
}
if (bulletListConfig == null) {
bulletListConfig = new BulletListItemSpan.Config(px(24), 0, px(8), px(1));
}
if (headingConfig == null) {
headingConfig = new HeadingSpan.Config(px(1), 0);
}
if (thematicConfig == null) {
thematicConfig = new ThematicBreakSpan.Config(0, px(2));
}
if (orderedListConfig == null) {
orderedListConfig = new OrderedListItemSpan.Config(px(24), 0);
if (theme == null) {
theme = SpannableTheme.create(context);
}
if (asyncDrawableLoader == null) {
asyncDrawableLoader = new AsyncDrawableLoaderNoOp();
}
if (syntaxHighlight == null) {
syntaxHighlight = new SyntaxHighlightNoOp();
}
if (linkResolver == null) {
linkResolver = new LinkResolverDef();
}
return new SpannableConfiguration(this);
}
private int px(int dp) {
return (int) (context.getResources().getDisplayMetrics().density * dp + .5F);
}
}
}

View File

@ -3,7 +3,6 @@ package ru.noties.markwon.renderer;
import android.support.annotation.NonNull;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.StrikethroughSpan;
import android.text.style.URLSpan;
@ -38,6 +37,7 @@ import ru.noties.markwon.spans.BulletListItemSpan;
import ru.noties.markwon.spans.CodeSpan;
import ru.noties.markwon.spans.EmphasisSpan;
import ru.noties.markwon.spans.HeadingSpan;
import ru.noties.markwon.spans.LinkSpan;
import ru.noties.markwon.spans.OrderedListItemSpan;
import ru.noties.markwon.spans.StrongEmphasisSpan;
import ru.noties.markwon.spans.ThematicBreakSpan;
@ -97,7 +97,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
visitChildren(blockQuote);
setSpan(length, new BlockQuoteSpan(
configuration.getBlockQuoteConfig(),
configuration.theme(),
blockQuoteIndent
));
@ -123,7 +123,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
builder.append('\u00a0');
setSpan(length, new CodeSpan(
configuration.getCodeConfig(),
configuration.theme(),
false
));
}
@ -131,17 +131,20 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
@Override
public void visit(FencedCodeBlock fencedCodeBlock) {
// Debug.i(fencedCodeBlock);
newLine();
final int length = builder.length();
// empty lines on top & bottom
builder.append('\u00a0').append('\n');
builder.append(fencedCodeBlock.getLiteral());
builder.append(
configuration.syntaxHighlight()
.highlight(fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral())
);
builder.append('\u00a0').append('\n');
setSpan(length, new CodeSpan(
configuration.getCodeConfig(),
configuration.theme(),
true
));
@ -189,7 +192,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
visitChildren(listItem);
setSpan(length, new OrderedListItemSpan(
configuration.getOrderedListConfig(),
configuration.theme(),
String.valueOf(start) + "." + '\u00a0',
blockQuoteIndent,
length
@ -204,7 +207,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
visitChildren(listItem);
setSpan(length, new BulletListItemSpan(
configuration.getBulletListConfig(),
configuration.theme(),
blockQuoteIndent,
listLevel - 1,
length
@ -226,7 +229,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
final int length = builder.length();
builder.append(' '); // without space it won't render
setSpan(length, new ThematicBreakSpan(configuration.getThematicConfig()));
setSpan(length, new ThematicBreakSpan(configuration.theme()));
newLine();
builder.append('\n');
@ -242,7 +245,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
final int length = builder.length();
visitChildren(heading);
setSpan(length, new HeadingSpan(
configuration.getHeadingConfig(),
configuration.theme(),
heading.getLevel(),
builder.length())
);
@ -315,7 +318,18 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
builder.append(' '); // breakable space
}
setSpan(length, new AsyncDrawableSpan(new AsyncDrawable(image.getDestination(), configuration.getAsyncDrawableLoader())));
final Node parent = image.getParent();
final boolean link = parent != null && parent instanceof Link;
setSpan(length, new AsyncDrawableSpan(
configuration.theme(),
new AsyncDrawable(
image.getDestination(),
configuration.asyncDrawableLoader()
),
AsyncDrawableSpan.ALIGN_BOTTOM,
link)
);
}
@Override
@ -329,7 +343,7 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
public void visit(Link link) {
final int length = builder.length();
visitChildren(link);
setSpan(length, new URLSpan(link.getDestination()));
setSpan(length, new LinkSpan(configuration.theme(), link.getDestination(), configuration.linkResolver()));
}
private void setSpan(int start, @NonNull Object span) {

View File

@ -14,6 +14,7 @@ public class SpannableRenderer {
// * 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...
public CharSequence render(@NonNull SpannableConfiguration configuration, @Nullable Node node) {
final CharSequence out;

View File

@ -0,0 +1,10 @@
package ru.noties.markwon.renderer;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public interface SyntaxHighlight {
@NonNull
CharSequence highlight(@Nullable String info, @NonNull String code);
}

View File

@ -0,0 +1,12 @@
package ru.noties.markwon.renderer;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
class SyntaxHighlightNoOp implements SyntaxHighlight {
@NonNull
@Override
public CharSequence highlight(@Nullable String info, @NonNull String code) {
return code;
}
}

View File

@ -3,7 +3,6 @@ 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;
@ -13,23 +12,39 @@ import android.text.style.ReplacementSpan;
@SuppressWarnings("WeakerAccess")
public class AsyncDrawableSpan extends ReplacementSpan {
@IntDef({ ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER })
@interface Alignment {}
@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; // will only center if drawable height is less than text line height
private final SpannableTheme theme;
private final AsyncDrawable drawable;
private final int alignment;
private final boolean replacementTextIsLink;
public AsyncDrawableSpan(@NonNull AsyncDrawable drawable) {
this(drawable, ALIGN_BOTTOM);
public AsyncDrawableSpan(@NonNull SpannableTheme theme, @NonNull AsyncDrawable drawable) {
this(theme, drawable, ALIGN_BOTTOM);
}
public AsyncDrawableSpan(@NonNull AsyncDrawable drawable, @Alignment int alignment) {
public AsyncDrawableSpan(
@NonNull SpannableTheme theme,
@NonNull AsyncDrawable drawable,
@Alignment int alignment) {
this(theme, drawable, alignment, false);
}
public AsyncDrawableSpan(
@NonNull SpannableTheme theme,
@NonNull AsyncDrawable drawable,
@Alignment int alignment,
boolean replacementTextIsLink) {
this.theme = theme;
this.drawable = drawable;
this.alignment = alignment;
this.replacementTextIsLink = replacementTextIsLink;
// additionally set intrinsic bounds if empty
final Rect rect = drawable.getBounds();
@ -66,8 +81,13 @@ public class AsyncDrawableSpan extends ReplacementSpan {
} else {
size = (int) (paint.measureText(text, start, end) + .5F);
// we will apply style here in case if theme modifies textSize or style (affects metrics)
if (replacementTextIsLink) {
theme.applyLinkStyle(paint);
}
// NB, no specific text handling (no new lines, etc)
size = (int) (paint.measureText(text, start, end) + .5F);
}
return size;
@ -108,7 +128,15 @@ public class AsyncDrawableSpan extends ReplacementSpan {
}
} else {
final int textY = (int) (bottom - ((bottom - top) / 2) - ((paint.descent() + paint.ascent()) / 2.F + .5F));
// will it make sense to have additional background/borders for an image replacement?
// let's focus on main functionality and then think of it
final float textY = CanvasUtils.textCenterY(top, bottom, paint);
if (replacementTextIsLink) {
theme.applyLinkStyle(paint);
}
// NB, no specific text handling (no new lines, etc)
canvas.drawText(text, start, end, x, textY, paint);
}
}

View File

@ -11,7 +11,7 @@ import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class DrawableSpanUtils {
public class AsyncDrawableSpanUtils {
// todo, add `unschedule` method (to be used when new text is set, so
// drawables are removed from callbacks)
@ -66,7 +66,7 @@ public class DrawableSpanUtils {
}
}
private DrawableSpanUtils() {}
private AsyncDrawableSpanUtils() {}
private static class DrawableCallbackImpl implements Drawable.Callback {

View File

@ -0,0 +1,51 @@
package ru.noties.markwon.spans;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.text.Layout;
import android.text.style.LeadingMarginSpan;
public class BlockQuoteSpan implements LeadingMarginSpan {
private final SpannableTheme theme;
private final Rect rect = ObjectsPool.rect();
private final Paint paint = ObjectsPool.paint();
private final int indent;
public BlockQuoteSpan(@NonNull SpannableTheme theme, int indent) {
this.theme = theme;
this.indent = indent;
}
@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) {
final int width = theme.getBlockQuoteWidth();
theme.applyBlockQuoteStyle(paint);
final int left = theme.getBlockMargin() * (indent - 1);
rect.set(left, top, left + width, bottom);
c.drawRect(rect, paint);
}
}

View File

@ -4,7 +4,6 @@ import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.annotation.ColorInt;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.text.Layout;
@ -12,30 +11,7 @@ import android.text.style.LeadingMarginSpan;
public class BulletListItemSpan implements LeadingMarginSpan {
// todo, there are 3 types of bullets: filled circle, stroke circle & filled rectangle
// also, there are ordered lists
public static class Config {
final int marginWidth;
final int bulletColor; // by default uses text color
final int bulletSide;
final int bulletStrokeWidth;
// from 0 but it makes sense to provide something wider
public Config(
@IntRange(from = 0) int marginWidth,
@ColorInt int bulletColor,
@IntRange(from = 0) int bulletSide,
@IntRange(from = 0) int bulletStrokeWidth) {
this.marginWidth = marginWidth;
this.bulletColor = bulletColor;
this.bulletSide = bulletSide;
this.bulletStrokeWidth = bulletStrokeWidth;
}
}
private final Config config;
private SpannableTheme theme;
private final Paint paint = ObjectsPool.paint();
private final RectF circle = ObjectsPool.rectF();
@ -46,11 +22,11 @@ public class BulletListItemSpan implements LeadingMarginSpan {
private final int start;
public BulletListItemSpan(
@NonNull Config config,
@NonNull SpannableTheme theme,
@IntRange(from = 0) int blockIndent,
@IntRange(from = 0) int level,
@IntRange(from = 0) int start) {
this.config = config;
this.theme = theme;
this.blockIndent = blockIndent;
this.level = level;
this.start = start;
@ -58,7 +34,7 @@ public class BulletListItemSpan implements LeadingMarginSpan {
@Override
public int getLeadingMargin(boolean first) {
return config.marginWidth;
return theme.getBlockMargin();
}
@Override
@ -69,39 +45,17 @@ public class BulletListItemSpan implements LeadingMarginSpan {
return;
}
final int color;
final float stroke;
paint.set(p);
if (config.bulletColor == 0) {
color = p.getColor();
} else {
color = config.bulletColor;
}
if (config.bulletStrokeWidth == 0) {
stroke = p.getStrokeWidth();
} else {
stroke = config.bulletStrokeWidth;
}
paint.setColor(color);
paint.setStrokeWidth(stroke);
theme.applyListItemStyle(paint);
final int save = c.save();
try {
// by default we use half of margin width, but if height is less than width, we calculate from it
final int width = config.marginWidth;
final int width = theme.getBlockMargin();
final int height = bottom - top;
final int min = Math.min(config.marginWidth, height) / 2;
final int side;
if (config.bulletSide == 0
|| config.bulletSide > min) {
side = min;
} else {
side = config.bulletSide;
}
final int side = theme.getBulletWidth(bottom - top);
final int marginLeft = (width - side) / 2;
final int marginTop = (height - side) / 2;

View File

@ -0,0 +1,14 @@
package ru.noties.markwon.spans;
import android.graphics.Paint;
import android.support.annotation.NonNull;
abstract class CanvasUtils {
static float textCenterY(int top, int bottom, @NonNull Paint paint) {
return (int) (bottom - ((bottom - top) / 2) - ((paint.descent() + paint.ascent()) / 2.F + .5F));
}
private CanvasUtils() {
}
}

View File

@ -0,0 +1,60 @@
package ru.noties.markwon.spans;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.text.Layout;
import android.text.TextPaint;
import android.text.style.LeadingMarginSpan;
import android.text.style.MetricAffectingSpan;
public class CodeSpan extends MetricAffectingSpan implements LeadingMarginSpan {
private final SpannableTheme theme;
private final Rect rect = ObjectsPool.rect();
private final Paint paint = ObjectsPool.paint();
private final boolean multiline;
public CodeSpan(@NonNull SpannableTheme theme, boolean multiline) {
this.theme = theme;
this.multiline = multiline;
}
@Override
public void updateMeasureState(TextPaint p) {
apply(p);
}
@Override
public void updateDrawState(TextPaint ds) {
apply(ds);
if (!multiline) {
ds.bgColor = theme.getCodeBackgroundColor(ds);
}
}
private void apply(TextPaint p) {
theme.applyCodeTextStyle(p);
}
@Override
public int getLeadingMargin(boolean first) {
return multiline ? theme.getCodeMultilineMargin() : 0;
}
@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 (multiline) {
paint.setStyle(Paint.Style.FILL);
paint.setColor(theme.getCodeBackgroundColor(p));
rect.set(x, top, c.getWidth(), bottom);
c.drawRect(rect, paint);
}
}
}

View File

@ -1,6 +1,6 @@
package ru.noties.markwon.spans;
class ColorUtils {
abstract class ColorUtils {
static int applyAlpha(int color, int alpha) {
return (color & 0x00FFFFFF) | (alpha << 24);

View File

@ -0,0 +1,67 @@
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.text.Layout;
import android.text.TextPaint;
import android.text.style.LeadingMarginSpan;
import android.text.style.MetricAffectingSpan;
public class HeadingSpan extends MetricAffectingSpan implements LeadingMarginSpan {
private final SpannableTheme theme;
private final Rect rect = ObjectsPool.rect();
private final Paint paint = ObjectsPool.paint();
private final int level;
private final int end;
public HeadingSpan(@NonNull SpannableTheme theme, @IntRange(from = 1, to = 6) int level, @IntRange(from = 0) int end) {
this.theme = theme;
this.level = level;
this.end = end;
}
@Override
public void updateMeasureState(TextPaint p) {
apply(p);
}
@Override
public void updateDrawState(TextPaint tp) {
apply(tp);
}
private void apply(TextPaint paint) {
theme.applyHeadingTextStyle(paint, level);
}
@Override
public int getLeadingMargin(boolean first) {
// no margin actually, but we need to access Canvas to draw break
return 0;
}
@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 (level == 1
|| level == 2) {
if (this.end == end) {
paint.set(p);
theme.applyHeadingBreakStyle(paint);
final float height = paint.getStrokeWidth();
final int b = (int) (bottom - height + .5F);
rect.set(x, b, c.getWidth(), bottom);
c.drawRect(rect, paint);
}
}
}
}

View File

@ -0,0 +1,33 @@
package ru.noties.markwon.spans;
import android.support.annotation.NonNull;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.View;
public class LinkSpan extends ClickableSpan {
public interface Resolver {
void resolve(View view, @NonNull String link);
}
private final SpannableTheme theme;
private final String link;
private final Resolver resolver;
public LinkSpan(@NonNull SpannableTheme theme, @NonNull String link, @NonNull Resolver resolver) {
this.theme = theme;
this.link = link;
this.resolver = resolver;
}
@Override
public void onClick(View widget) {
resolver.resolve(widget, link);
}
@Override
public void updateDrawState(TextPaint ds) {
theme.applyLinkStyle(ds);
}
}

View File

@ -2,7 +2,6 @@ package ru.noties.markwon.spans;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.ColorInt;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.text.Layout;
@ -10,29 +9,18 @@ import android.text.style.LeadingMarginSpan;
public class OrderedListItemSpan implements LeadingMarginSpan {
public static class Config {
final int marginWidth; // by default 0
final int numberColor; // by default color of the main text
public Config(@IntRange(from = 0) int marginWidth, @ColorInt int numberColor) {
this.marginWidth = marginWidth;
this.numberColor = numberColor;
}
}
private final Config config;
private final SpannableTheme theme;
private final String number;
private final int blockIndent;
private final int start;
public OrderedListItemSpan(
@NonNull Config config,
@NonNull SpannableTheme theme,
@NonNull String number,
@IntRange(from = 0) int blockIndent,
@IntRange(from = 0) int start
) {
this.config = config;
this.theme = theme;
this.number = number;
this.blockIndent = blockIndent;
this.start = start;
@ -40,7 +28,7 @@ public class OrderedListItemSpan implements LeadingMarginSpan {
@Override
public int getLeadingMargin(boolean first) {
return config.marginWidth;
return theme.getBlockMargin();
}
@Override
@ -51,15 +39,13 @@ public class OrderedListItemSpan implements LeadingMarginSpan {
return;
}
if (config.numberColor != 0) {
p.setColor(config.numberColor);
}
theme.applyListItemStyle(p);
final int width = config.marginWidth;
final int width = theme.getBlockMargin();
final int numberWidth = (int) (p.measureText(number) + .5F);
final int numberX = (width * blockIndent) - numberWidth;
final int numberY = bottom - ((bottom - top) / 2) - (int) ((p.descent() + p.ascent()) / 2);
final float numberY = CanvasUtils.textCenterY(top, bottom, p);
c.drawText(number, numberX, numberY, p);
}

View File

@ -0,0 +1,456 @@
package ru.noties.markwon.spans;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.support.annotation.AttrRes;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.text.TextPaint;
import android.util.TypedValue;
import android.widget.TextView;
@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)
public static SpannableTheme create(@NonNull Context context) {
return builderWithDefaults(context).build();
}
public static Builder builder() {
return new Builder();
}
public static Builder builder(@NonNull SpannableTheme copyFrom) {
return new Builder(copyFrom);
}
public static Builder builderWithDefaults(@NonNull Context context) {
final Px px = new Px(context);
return new Builder()
.linkColor(resolve(context, android.R.attr.textColorLink))
.codeMultilineMargin(px.px(8))
.blockMargin(px.px(24))
.bulletListItemStrokeWidth(px.px(1))
.headingBreakHeight(px.px(1))
.thematicBreakHeight(px.px(2));
}
private static int resolve(Context context, @AttrRes int attr) {
final TypedValue typedValue = new TypedValue();
final int attrs[] = new int[]{attr};
final TypedArray typedArray = context.obtainStyledAttributes(typedValue.data, attrs);
try {
return typedArray.getColor(0, 0);
} finally {
typedArray.recycle();
}
}
protected static final int BLOCK_QUOTE_DEF_COLOR_ALPHA = 50;
protected static final int CODE_DEF_BACKGROUND_COLOR_ALPHA = 25;
protected static final float CODE_DEF_TEXT_SIZE_RATIO = .87F;
protected static final int HEADING_DEF_BREAK_COLOR_ALPHA = 75;
// taken from html spec (most browsers render headings like that)
// is not exposed via protected modifier in order to disallow modification
private static final float[] HEADING_SIZES = {
2.F, 1.5F, 1.17F, 1.F, .83F, .67F,
};
protected static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F;
protected static final int THEMATIC_BREAK_DEF_ALPHA = 75;
protected final int linkColor;
// used in quote, lists
protected final int blockMargin;
// by default it's 1/4th of `blockMargin`
protected final int blockQuoteWidth;
// by default it's text color with `BLOCK_QUOTE_DEF_COLOR_ALPHA` applied alpha
protected final int blockQuoteColor;
// by default uses text color (applied for un-ordered lists & ordered (bullets & numbers)
protected final int listItemColor;
// by default the stroke color of a paint object
protected final int bulletListItemStrokeWidth;
// width of bullet, by default min(blockMargin, height) / 2
protected final int bulletWidth;
// by default - main text color
protected final int codeTextColor;
// by default 0.1 alpha of textColor/codeTextColor
protected final int codeBackgroundColor;
// by default `width` of a space char... it's fun and games, but span doesn't have access to paint in `getLeadingMargin`
// so, we need to set this value explicitly (think of an utility method, that takes TextView/TextPaint and measures space char)
protected final int codeMultilineMargin;
// by default Typeface.MONOSPACE
protected final Typeface codeTypeface;
// by default a bit (how much?!) smaller than normal text
// applied ONLY if default typeface was used, otherwise, not applied
protected final int codeTextSize;
// by default paint.getStrokeWidth
protected final int headingBreakHeight;
// by default, text color with `HEADING_DEF_BREAK_COLOR_ALPHA` applied alpha
protected final int headingBreakColor;
// by default `SCRIPT_DEF_TEXT_SIZE_RATIO`
protected final float scriptTextSizeRatio;
// by default textColor with `THEMATIC_BREAK_DEF_ALPHA` applied alpha
protected final int thematicBreakColor;
// by default paint.strokeWidth
protected final int thematicBreakHeight;
protected SpannableTheme(@NonNull Builder builder) {
this.linkColor = builder.linkColor;
this.blockMargin = builder.blockMargin;
this.blockQuoteWidth = builder.blockQuoteWidth;
this.blockQuoteColor = builder.blockQuoteColor;
this.listItemColor = builder.listItemColor;
this.bulletListItemStrokeWidth = builder.bulletListItemStrokeWidth;
this.bulletWidth = builder.bulletWidth;
this.codeTextColor = builder.codeTextColor;
this.codeBackgroundColor = builder.codeBackgroundColor;
this.codeMultilineMargin = builder.codeMultilineMargin;
this.codeTypeface = builder.codeTypeface;
this.codeTextSize = builder.codeTextSize;
this.headingBreakHeight = builder.headingBreakHeight;
this.headingBreakColor = builder.headingBreakColor;
this.scriptTextSizeRatio = builder.scriptTextSizeRatio;
this.thematicBreakColor = builder.thematicBreakColor;
this.thematicBreakHeight = builder.thematicBreakHeight;
}
public void applyLinkStyle(@NonNull Paint paint) {
paint.setUnderlineText(true);
if (linkColor != 0) {
// by default we will be using text color
paint.setColor(linkColor);
}
}
public void applyBlockQuoteStyle(@NonNull Paint paint) {
final int color;
if (blockQuoteColor == 0) {
color = ColorUtils.applyAlpha(paint.getColor(), BLOCK_QUOTE_DEF_COLOR_ALPHA);
} else {
color = blockQuoteColor;
}
paint.setStyle(Paint.Style.FILL);
paint.setColor(color);
}
public int getBlockMargin() {
return blockMargin;
}
public int getBlockQuoteWidth() {
final int out;
if (blockQuoteWidth == 0) {
out = (int) (blockMargin * .25F + .5F);
} else {
out = blockQuoteWidth;
}
return out;
}
public void applyListItemStyle(@NonNull Paint paint) {
final int color;
if (listItemColor != 0) {
color = listItemColor;
} else {
color = paint.getColor();
}
paint.setColor(color);
if (bulletListItemStrokeWidth != 0) {
paint.setStrokeWidth(bulletListItemStrokeWidth);
}
}
public int getBulletWidth(int height) {
final int min = Math.min(blockMargin, height) / 2;
final int width;
if (bulletWidth == 0
|| bulletWidth > min) {
width = min;
} else {
width = bulletWidth;
}
return width;
}
public void applyCodeTextStyle(@NonNull Paint paint) {
if (codeTextColor != 0) {
paint.setColor(codeTextColor);
}
// custom typeface was set
if (codeTypeface != null) {
paint.setTypeface(codeTypeface);
if (codeTextSize != 0) {
paint.setTextSize(codeTextSize);
}
} else {
paint.setTypeface(Typeface.MONOSPACE);
final float textSize;
if (codeTextSize != 0) {
textSize = codeTextSize;
} else {
textSize = paint.getTextSize() * CODE_DEF_TEXT_SIZE_RATIO;
}
paint.setTextSize(textSize);
}
}
public int getCodeMultilineMargin() {
return codeMultilineMargin;
}
public int getCodeBackgroundColor(@NonNull Paint paint) {
final int color;
if (codeBackgroundColor != 0) {
color = codeBackgroundColor;
} else {
color = ColorUtils.applyAlpha(paint.getColor(), CODE_DEF_BACKGROUND_COLOR_ALPHA);
}
return color;
}
public void applyHeadingTextStyle(@NonNull Paint paint, @IntRange(from = 1, to = 6) int level) {
paint.setFakeBoldText(true);
paint.setTextSize(paint.getTextSize() * HEADING_SIZES[level - 1]);
}
public void applyHeadingBreakStyle(@NonNull Paint paint) {
final int color;
if (headingBreakColor != 0) {
color = headingBreakColor;
} else {
color = ColorUtils.applyAlpha(paint.getColor(), HEADING_DEF_BREAK_COLOR_ALPHA);
}
paint.setColor(color);
paint.setStyle(Paint.Style.FILL);
if (headingBreakHeight != 0) {
//noinspection SuspiciousNameCombination
paint.setStrokeWidth(headingBreakHeight);
}
}
public void applySuperScriptStyle(@NonNull TextPaint paint) {
final float ratio;
if (Float.compare(scriptTextSizeRatio, .0F) == 0) {
ratio = SCRIPT_DEF_TEXT_SIZE_RATIO;
} else {
ratio = scriptTextSizeRatio;
}
paint.setTextSize(paint.getTextSize() * ratio);
paint.baselineShift += (int) (paint.ascent() / 2);
}
public void applySubScriptStyle(@NonNull TextPaint paint) {
final float ratio;
if (Float.compare(scriptTextSizeRatio, .0F) == 0) {
ratio = SCRIPT_DEF_TEXT_SIZE_RATIO;
} else {
ratio = scriptTextSizeRatio;
}
paint.setTextSize(paint.getTextSize() * ratio);
paint.baselineShift -= (int) (paint.ascent() / 2);
}
public void applyThematicBreakStyle(@NonNull Paint paint) {
final int color;
if (thematicBreakColor != 0) {
color = thematicBreakColor;
} else {
color = ColorUtils.applyAlpha(paint.getColor(), THEMATIC_BREAK_DEF_ALPHA);
}
paint.setColor(color);
paint.setStyle(Paint.Style.FILL);
if (thematicBreakHeight != 0) {
//noinspection SuspiciousNameCombination
paint.setStrokeWidth(thematicBreakHeight);
}
}
public static class Builder {
private int linkColor;
private int blockMargin;
private int blockQuoteWidth;
private int blockQuoteColor;
private int listItemColor;
private int bulletListItemStrokeWidth;
private int bulletWidth;
private int codeTextColor;
private int codeBackgroundColor;
private int codeMultilineMargin;
private Typeface codeTypeface;
private int codeTextSize;
private int headingBreakHeight;
private int headingBreakColor;
private float scriptTextSizeRatio;
private int thematicBreakColor;
private int thematicBreakHeight;
Builder() {
}
Builder(@NonNull SpannableTheme theme) {
this.linkColor = theme.linkColor;
this.blockMargin = theme.blockMargin;
this.blockQuoteWidth = theme.blockQuoteWidth;
this.blockQuoteColor = theme.blockQuoteColor;
this.listItemColor = theme.listItemColor;
this.bulletListItemStrokeWidth = theme.bulletListItemStrokeWidth;
this.bulletWidth = theme.bulletWidth;
this.codeTextColor = theme.codeTextColor;
this.codeBackgroundColor = theme.codeBackgroundColor;
this.codeMultilineMargin = theme.codeMultilineMargin;
this.codeTypeface = theme.codeTypeface;
this.codeTextSize = theme.codeTextSize;
this.headingBreakHeight = theme.headingBreakHeight;
this.headingBreakColor = theme.headingBreakColor;
this.scriptTextSizeRatio = theme.scriptTextSizeRatio;
this.thematicBreakColor = theme.thematicBreakColor;
this.thematicBreakHeight = theme.thematicBreakHeight;
}
public Builder linkColor(int linkColor) {
this.linkColor = linkColor;
return this;
}
public Builder blockMargin(int blockMargin) {
this.blockMargin = blockMargin;
return this;
}
public Builder blockQuoteWidth(int blockQuoteWidth) {
this.blockQuoteWidth = blockQuoteWidth;
return this;
}
public Builder blockQuoteColor(int blockQuoteColor) {
this.blockQuoteColor = blockQuoteColor;
return this;
}
public Builder listItemColor(int listItemColor) {
this.listItemColor = listItemColor;
return this;
}
public Builder bulletListItemStrokeWidth(int bulletListItemStrokeWidth) {
this.bulletListItemStrokeWidth = bulletListItemStrokeWidth;
return this;
}
public Builder bulletWidth(int bulletWidth) {
this.bulletWidth = bulletWidth;
return this;
}
public Builder codeTextColor(int codeTextColor) {
this.codeTextColor = codeTextColor;
return this;
}
public Builder codeBackgroundColor(int codeBackgroundColor) {
this.codeBackgroundColor = codeBackgroundColor;
return this;
}
public Builder codeMultilineMargin(int codeMultilineMargin) {
this.codeMultilineMargin = codeMultilineMargin;
return this;
}
public Builder codeTypeface(Typeface codeTypeface) {
this.codeTypeface = codeTypeface;
return this;
}
public Builder codeTextSize(int codeTextSize) {
this.codeTextSize = codeTextSize;
return this;
}
public Builder headingBreakHeight(int headingBreakHeight) {
this.headingBreakHeight = headingBreakHeight;
return this;
}
public Builder headingBreakColor(int headingBreakColor) {
this.headingBreakColor = headingBreakColor;
return this;
}
public Builder scriptTextSizeRatio(float scriptTextSizeRatio) {
this.scriptTextSizeRatio = scriptTextSizeRatio;
return this;
}
public Builder thematicBreakColor(int thematicBreakColor) {
this.thematicBreakColor = thematicBreakColor;
return this;
}
public Builder thematicBreakHeight(int thematicBreakHeight) {
this.thematicBreakHeight = thematicBreakHeight;
return this;
}
public SpannableTheme build() {
return new SpannableTheme(this);
}
}
private static class Px {
private final float density;
Px(@NonNull Context context) {
this.density = context.getResources().getDisplayMetrics().density;
}
int px(int dp) {
return (int) (dp * density + .5F);
}
}
}

View File

@ -0,0 +1,28 @@
package ru.noties.markwon.spans;
import android.support.annotation.NonNull;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
public class SubScriptSpan extends MetricAffectingSpan {
private final SpannableTheme theme;
public SubScriptSpan(@NonNull SpannableTheme theme) {
this.theme = theme;
}
@Override
public void updateDrawState(TextPaint tp) {
apply(tp);
}
@Override
public void updateMeasureState(TextPaint tp) {
apply(tp);
}
private void apply(TextPaint paint) {
theme.applySubScriptStyle(paint);
}
}

View File

@ -0,0 +1,28 @@
package ru.noties.markwon.spans;
import android.support.annotation.NonNull;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
public class SuperScriptSpan extends MetricAffectingSpan {
private final SpannableTheme theme;
public SuperScriptSpan(@NonNull SpannableTheme theme) {
this.theme = theme;
}
@Override
public void updateDrawState(TextPaint tp) {
apply(tp);
}
@Override
public void updateMeasureState(TextPaint tp) {
apply(tp);
}
private void apply(TextPaint paint) {
theme.applySuperScriptStyle(paint);
}
}

View File

@ -0,0 +1,39 @@
package ru.noties.markwon.spans;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.text.Layout;
import android.text.style.LeadingMarginSpan;
public class ThematicBreakSpan implements LeadingMarginSpan {
private final SpannableTheme theme;
private final Rect rect = ObjectsPool.rect();
private final Paint paint = ObjectsPool.paint();
public ThematicBreakSpan(@NonNull SpannableTheme theme) {
this.theme = theme;
}
@Override
public int getLeadingMargin(boolean first) {
return 0;
}
@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 = top + ((bottom - top) / 2);
paint.set(p);
theme.applyThematicBreakStyle(paint);
final int height = (int) (paint.getStrokeWidth() + .5F);
final int halfHeight = (int) (height / 2.F + .5F);
rect.set(x, middle - halfHeight, c.getWidth(), middle + halfHeight);
c.drawRect(rect, paint);
}
}

View File

@ -1,18 +0,0 @@
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
}

View File

@ -1 +0,0 @@
<manifest package="ru.noties.markwon.spans" />

View File

@ -1,84 +0,0 @@
package ru.noties.markwon.spans;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.ColorInt;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.text.Layout;
import android.text.style.LeadingMarginSpan;
public class BlockQuoteSpan implements LeadingMarginSpan {
private static final int DEF_COLOR_ALPHA = 50;
@SuppressWarnings("WeakerAccess")
public static class Config {
final int totalWidth;
final int quoteWidth; // by default 1/4 of width
final int quoteColor; // by default textColor with 0.2 alpha
public Config(
@IntRange(from = 1) int totalWidth,
@IntRange(from = 0) int quoteWidth,
@ColorInt int quoteColor) {
this.totalWidth = totalWidth;
this.quoteWidth = quoteWidth;
this.quoteColor = quoteColor;
}
}
private final Config config;
private final Rect rect = ObjectsPool.rect();
private final Paint paint = ObjectsPool.paint();
private final int indent;
public BlockQuoteSpan(@NonNull Config config, int indent) {
this.config = config;
this.indent = indent;
}
@Override
public int getLeadingMargin(boolean first) {
return config.totalWidth;
}
@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 width;
if (config.quoteWidth == 0) {
width = (int) (config.totalWidth / 4.F + .5F);
} else {
width = config.quoteWidth;
}
final int color;
if (config.quoteColor != 0) {
color = config.quoteColor;
} else {
color = ColorUtils.applyAlpha(p.getColor(), DEF_COLOR_ALPHA);
}
paint.setStyle(Paint.Style.FILL);
paint.setColor(color);
final int left = config.totalWidth * (indent - 1);
rect.set(left, top, left + width, bottom);
c.drawRect(rect, paint);
}
}

View File

@ -1,146 +0,0 @@
package ru.noties.markwon.spans;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.support.annotation.ColorInt;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.text.Layout;
import android.text.TextPaint;
import android.text.style.LeadingMarginSpan;
import android.text.style.MetricAffectingSpan;
public class CodeSpan extends MetricAffectingSpan implements LeadingMarginSpan {
private static final int DEF_COLOR_ALPHA = 25;
@SuppressWarnings("WeakerAccess")
public static class Config {
public static Builder builder() {
return new Builder();
}
final int textColor; // by default the same as main text
final int backgroundColor; // by default textColor with 0.1 alpha
final int multilineMargin; // by default 0
final int textSize; // by default the same as main text
final Typeface typeface; // by default Typeface.MONOSPACE
private Config(Builder builder) {
this.textColor = builder.textColor;
this.backgroundColor = builder.backgroundColor;
this.multilineMargin = builder.multilineMargin;
this.textSize = builder.textSize;
this.typeface = builder.typeface;
}
public static class Builder {
int textColor;
int backgroundColor;
int multilineMargin;
int textSize;
Typeface typeface;
public Builder setTextColor(@ColorInt int textColor) {
this.textColor = textColor;
return this;
}
public Builder setBackgroundColor(@ColorInt int backgroundColor) {
this.backgroundColor = backgroundColor;
return this;
}
public Builder setMultilineMargin(int multilineMargin) {
this.multilineMargin = multilineMargin;
return this;
}
public Builder setTextSize(@IntRange(from = 0) int textSize) {
this.textSize = textSize;
return this;
}
public Builder setTypeface(@NonNull Typeface typeface) {
this.typeface = typeface;
return this;
}
public Config build() {
if (typeface == null) {
typeface = Typeface.MONOSPACE;
}
return new Config(this);
}
}
}
private final Config config;
private final Rect rect = ObjectsPool.rect();
private final Paint paint = ObjectsPool.paint();
private final boolean multiline;
public CodeSpan(@NonNull Config config, boolean multiline) {
this.config = config;
this.multiline = multiline;
}
@Override
public void updateMeasureState(TextPaint p) {
apply(p);
}
@Override
public void updateDrawState(TextPaint ds) {
apply(ds);
if (!multiline) {
final int color;
if (config.backgroundColor == 0) {
color = ColorUtils.applyAlpha(ds.getColor(), DEF_COLOR_ALPHA);
} else {
color = config.backgroundColor;
}
ds.bgColor = color;
}
}
private void apply(TextPaint p) {
p.setTypeface(config.typeface);
if (config.textSize > 0) {
p.setTextSize(config.textSize);
}
if (config.textColor != 0) {
p.setColor(config.textColor);
}
}
@Override
public int getLeadingMargin(boolean first) {
return multiline ? config.multilineMargin : 0;
}
@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 (multiline) {
final int color;
if (config.backgroundColor == 0) {
color = ColorUtils.applyAlpha(p.getColor(), DEF_COLOR_ALPHA);
} else {
color = config.backgroundColor;
}
paint.setStyle(Paint.Style.FILL);
paint.setColor(color);
rect.set(x, top, c.getWidth(), bottom);
c.drawRect(rect, paint);
}
}
}

View File

@ -1,97 +0,0 @@
package ru.noties.markwon.spans;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.ColorInt;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.text.Layout;
import android.text.TextPaint;
import android.text.style.LeadingMarginSpan;
import android.text.style.MetricAffectingSpan;
public class HeadingSpan extends MetricAffectingSpan implements LeadingMarginSpan {
// taken from html spec (most browsers render headings like that)
private static final float[] HEADING_SIZES = {
2.F, 1.5F, 1.17F, 1.F, .83F, .67F,
};
private static final int DEF_BREAK_COLOR_ALPHA = 127;
public static class Config {
final int breakHeight; // by default stroke width
final int breakColor; // by default -> textColor with 0.5 alpha
public Config(@IntRange(from = 0) int breakHeight, @ColorInt int breakColor) {
this.breakHeight = breakHeight;
this.breakColor = breakColor;
}
}
private final Config config;
private final Rect rect = ObjectsPool.rect();
private final Paint paint = ObjectsPool.paint();
private final int level;
private final int end;
public HeadingSpan(@NonNull Config config, @IntRange(from = 1, to = 6) int level, @IntRange(from = 0) int end) {
this.config = config;
this.level = level - 1;
this.end = end;
}
@Override
public void updateMeasureState(TextPaint p) {
apply(p);
}
@Override
public void updateDrawState(TextPaint tp) {
apply(tp);
}
private void apply(TextPaint paint) {
paint.setTextSize(paint.getTextSize() * HEADING_SIZES[level]);
paint.setFakeBoldText(true);
}
@Override
public int getLeadingMargin(boolean first) {
// no margin actually, but we need to access Canvas to draw break
return 0;
}
@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 we are configured to draw underlines, draw them here
if (level == 0
|| level == 1) {
if (this.end == end) {
final int color;
final int breakHeight;
if (config.breakColor == 0) {
color = ColorUtils.applyAlpha(p.getColor(), DEF_BREAK_COLOR_ALPHA);
} else {
color = config.breakColor;
}
if (config.breakHeight == 0) {
breakHeight = (int) (p.getStrokeWidth() + .5F);
} else {
breakHeight = config.breakHeight;
}
paint.setStyle(Paint.Style.FILL);
paint.setColor(color);
rect.set(x, bottom - breakHeight, c.getWidth(), bottom);
c.drawRect(rect, paint);
}
}
}
}

View File

@ -1,19 +0,0 @@
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);
}
}

View File

@ -1,19 +0,0 @@
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);
}
}

View File

@ -1,64 +0,0 @@
package ru.noties.markwon.spans;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.ColorInt;
import android.support.annotation.IntRange;
import android.text.Layout;
import android.text.style.LeadingMarginSpan;
public class ThematicBreakSpan implements LeadingMarginSpan {
private static final int DEF_COLOR_ALPHA = 127;
public static class Config {
final int color; // by default textColor with 0.5 alpha
final int height; // by default strokeWidth of paint
public Config(@ColorInt int color, @IntRange(from = 0) int height) {
this.color = color;
this.height = height;
}
}
private final Config config;
private final Rect rect = ObjectsPool.rect();
private final Paint paint = ObjectsPool.paint();
public ThematicBreakSpan(Config config) {
this.config = config;
}
@Override
public int getLeadingMargin(boolean first) {
return 0;
}
@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 = top + ((bottom - top) / 2);
final int color;
if (config.color == 0) {
color = ColorUtils.applyAlpha(p.getColor(), DEF_COLOR_ALPHA);
} else {
color = config.color;
}
paint.setColor(color);
paint.setStyle(Paint.Style.FILL);
final int height;
if (config.height == 0) {
height = (int) (p.getStrokeWidth() + .5F);
} else {
height = config.height;
}
final int halfHeight = (int) (height / 2.F + .5F);
rect.set(x, middle - halfHeight, c.getWidth(), middle + halfHeight);
c.drawRect(rect, paint);
}
}

View File

@ -1 +1 @@
include ':app', ':library-spans', ':library-renderer', ':library-view'
include ':app', ':library-renderer', ':library-view'