2017-05-16 17:55:33 +03:00

457 lines
15 KiB
Java

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