Create additional spans

This commit is contained in:
chengjunzhang61 2021-12-08 09:59:36 -05:00
parent 6006812f56
commit c928750ff0
17 changed files with 802 additions and 0 deletions

1
markwon-ext-spans/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,37 @@
plugins {
id 'com.android.library'
}
android {
compileSdkVersion 31
defaultConfig {
minSdkVersion 21
targetSdkVersion 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

View File

21
markwon-ext-spans/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,26 @@
package io.noties.markwon.ext;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("io.noties.markwon.ext.test", appContext.getPackageName());
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.noties.markwon.ext">
</manifest>

View File

@ -0,0 +1,82 @@
package io.noties.markwon.;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.Layout;
import android.text.Spanned;
import android.text.style.LeadingMarginSpan;
import android.text.style.LineHeightSpan;
import io.noties.markwon.utils.ColorUtils;
public class BlockQuoteSpan implements LeadingMarginSpan, LineHeightSpan {
protected int BLOCK_QUOTE_DEF_COLOR_ALPHA = 100;
protected static final int BLOCK_QUOTE_WIDTH = 10;
public static int BLOCK_QUOTE_MARGIN = 100;
protected int BLOCK_COLOR = Color.LTGRAY;
private final int VERTICAL_SPACING = 20;
private final Rect rect = ObjectsPool.rect();
private final Paint paint = ObjectsPool.paint();
public BlockQuoteSpan() {
}
@Override
public int getLeadingMargin(boolean first) {
return BLOCK_QUOTE_MARGIN;
}
@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 = BLOCK_QUOTE_WIDTH;
paint.set(p);
applyBlockQuoteStyle(paint);
rect.set(x, top, x + dir * width, bottom);
c.drawRect(rect, paint);
}
public void applyBlockQuoteStyle(Paint paint) {
final int color = ColorUtils.applyAlpha(BLOCK_COLOR, BLOCK_QUOTE_DEF_COLOR_ALPHA);
paint.setStyle(Paint.Style.FILL);
paint.setColor(color);
}
@Override
public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt fm) {
final Spanned txt = (Spanned)text;
final int spanEnd = txt.getSpanEnd(this);
final int spanStart = txt.getSpanStart(this);
// add top spacing to first line
if (start == spanStart) {
fm.ascent -= VERTICAL_SPACING;
fm.top -= VERTICAL_SPACING;
}
// add bottom spacing to last line
if (Math.abs(spanEnd - end) <= 1) {
fm.descent += VERTICAL_SPACING;
fm.bottom += VERTICAL_SPACING;
}
}
}

View File

@ -0,0 +1,190 @@
package io.noties.markwon.span.ext;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.os.Parcel;
import android.text.Layout;
import android.text.ParcelableSpan;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.LeadingMarginSpan;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.ColorInt;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import com.campuswire.android.messenger.model.THEMETYPE;
public class BulletSpan implements LeadingMarginSpan, ParcelableSpan {
// Bullet is slightly bigger to avoid aliasing artifacts on mdpi devices.
private static final int STANDARD_BULLET_RADIUS = 6;
public static final int STANDARD_GAP_WIDTH = 2;
private static final int STANDARD_COLOR = 0;
public static final int BULLET_SPAN = 8;
@Px
private final int mGapWidth;
@Px
private final int mBulletRadius;
private Path mBulletPath = null;
@ColorInt
private final int mColor;
private final boolean mWantColor;
/**
* Creates a {@link android.text.style.BulletSpan} with the default values.
*/
public BulletSpan() {
this(STANDARD_GAP_WIDTH, STANDARD_COLOR, false, STANDARD_BULLET_RADIUS);
}
/**
* Creates a {@link android.text.style.BulletSpan} based on a gap width
*
* @param gapWidth the distance, in pixels, between the bullet point and the paragraph.
*/
public BulletSpan(int gapWidth) {
this(gapWidth, STANDARD_COLOR, false, STANDARD_BULLET_RADIUS);
}
public BulletSpan(int gapWidth, @ColorInt int color) {
this(gapWidth, color, true, STANDARD_BULLET_RADIUS);
}
public BulletSpan(int gapWidth, @ColorInt int color, @IntRange(from = 0) int bulletRadius) {
this(gapWidth, color, true, bulletRadius);
}
private BulletSpan(int gapWidth, @ColorInt int color, boolean wantColor,
@IntRange(from = 0) int bulletRadius) {
mGapWidth = gapWidth;
mBulletRadius = bulletRadius;
mColor = color;
mWantColor = wantColor;
}
public BulletSpan(@NonNull Parcel src) {
mGapWidth = src.readInt();
mWantColor = src.readInt() != 0;
mColor = src.readInt();
mBulletRadius = src.readInt();
}
@Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
public int getSpanTypeIdInternal() {
return BULLET_SPAN;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
dest.writeInt(mGapWidth);
dest.writeInt(mWantColor ? 1 : 0);
dest.writeInt(mColor);
dest.writeInt(mBulletRadius);
}
@Override
public int getLeadingMargin(boolean first) {
return 2 * mBulletRadius + mGapWidth;
}
/**
* Get the distance, in pixels, between the bullet point and the paragraph.
*
* @return the distance, in pixels, between the bullet point and the paragraph.
*/
public int getGapWidth() {
return mGapWidth;
}
/**
* Get the radius, in pixels, of the bullet point.
*
* @return the radius, in pixels, of the bullet point.
*/
public int getBulletRadius() {
return mBulletRadius;
}
/**
* Get the bullet point color.
*
* @return the bullet point color
*/
public int getColor() {
return mColor;
}
@Override
public void drawLeadingMargin(@NonNull Canvas canvas, @NonNull Paint paint, int x, int dir,
int top, int baseline, int bottom,
@NonNull CharSequence text, int start, int end,
boolean first, @Nullable Layout layout) {
if (((Spanned) text).getSpanStart(this) == start) {
Paint.Style style = paint.getStyle();
int oldcolor = 0;
if (mWantColor) {
oldcolor = paint.getColor();
paint.setColor(mColor);
}
paint.setStyle(Paint.Style.FILL);
if (layout != null) {
// "bottom" position might include extra space as a result of line spacing
// configuration. Subtract extra space in order to show bullet in the vertical
// center of characters.
final int line = layout.getLineForOffset(start);
bottom = bottom - 0;//layout.getLineExtra(line);
}
final float yPosition = (top + bottom) / 2f;
final float xPosition = x + dir * mBulletRadius;
if (canvas.isHardwareAccelerated()) {
if (mBulletPath == null) {
mBulletPath = new Path();
mBulletPath.addCircle(0.0f, 0.0f, mBulletRadius, Path.Direction.CW);
}
canvas.save();
canvas.translate(xPosition, yPosition);
canvas.drawPath(mBulletPath, paint);
canvas.restore();
} else {
canvas.drawCircle(xPosition, yPosition, mBulletRadius, paint);
}
if (mWantColor) {
paint.setColor(oldcolor);
}
paint.setStyle(style);
}
}
}

View File

@ -0,0 +1,28 @@
package io.noties.markwon.span.ext;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.View;
import androidx.annotation.NonNull;
import com.campuswire.android.messenger.interfaces.MessageItemTapListener;
public class CWClickableSpan extends ClickableSpan {
String userId;
MessageItemTapListener messageItemTapListener;
public CWClickableSpan(String text, MessageItemTapListener listener) {
super();
userId = text;
messageItemTapListener = listener;
}
@Override
public void onClick(@NonNull View view) {
messageItemTapListener.onClickUser(userId);
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
ds.setUnderlineText(false);
}
}

View File

@ -0,0 +1,31 @@
package io.noties.markwon.span.ext;
import android.graphics.drawable.Drawable;
import android.text.style.DynamicDrawableSpan;
public class CWDrawableSpan extends DynamicDrawableSpan {
private Drawable drawable;
private int requiredWidth = 0;
private int requiredHeight = 0;
public CWDrawableSpan(Drawable drawable){
this.drawable = drawable;
}
public CWDrawableSpan(Drawable drawable, int requiredWidth, int requiredHeight){
this.requiredWidth = requiredWidth;
this.requiredHeight = requiredHeight;
this.drawable = drawable;
}
@Override
public Drawable getDrawable() {
// drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
if (requiredHeight != 0 && requiredWidth != 0) {
drawable.setBounds(0, 0, requiredWidth, requiredHeight);
} else {
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
}
return drawable;
}
}

View File

@ -0,0 +1,57 @@
package io.noties.markwon.span.ext;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import android.text.style.DynamicDrawableSpan;
import android.text.style.ImageSpan;
import java.lang.ref.WeakReference;
public class CenteredImageSpan extends ImageSpan {
private WeakReference<Drawable> mDrawableRef;
public CenteredImageSpan(Context context, final int drawableRes) {
super(context, drawableRes);
}
public CenteredImageSpan(Drawable drawable) {
super(drawable);
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end,
Paint.FontMetricsInt fontMetricsInt) {
Drawable drawable = getDrawable();
Rect rect = drawable.getBounds();
if (fontMetricsInt != null) {
Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
int fontHeight = fmPaint.descent - fmPaint.ascent;
int drHeight = rect.bottom - rect.top;
int centerY = fmPaint.ascent + fontHeight / 2;
fontMetricsInt.ascent = centerY - drHeight / 2;
fontMetricsInt.top = fontMetricsInt.ascent;
fontMetricsInt.bottom = centerY + drHeight / 2;
fontMetricsInt.descent = fontMetricsInt.bottom;
}
return rect.right;
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end,
float x, int top, int y, int bottom, Paint paint) {
Drawable drawable = getDrawable();
canvas.save();
Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
int fontHeight = fmPaint.descent - fmPaint.ascent;
int centerY = y + fmPaint.descent - fontHeight / 2;
int transY = centerY - (drawable.getBounds().bottom - drawable.getBounds().top) / 2;
canvas.translate(x, transY);
drawable.draw(canvas);
canvas.restore();
}
}

View File

@ -0,0 +1,133 @@
package io.noties.markwon.span.ext;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.text.Layout;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.LeadingMarginSpan;
import android.text.style.LineHeightSpan;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.NonNull;
import com.campuswire.android.messenger.model.THEMETYPE;
import io.noties.markwon.core.MarkwonTheme;
public class CodeBlockSpan extends MetricAffectingSpan implements LeadingMarginSpan, LineHeightSpan {
protected static final float CODE_DEF_TEXT_SIZE_RATIO = 1.0F;
protected static final int DEFAULT_LEADING_MARGIN = 20;
protected int BACKGROUND_COLOR;
protected int TEXT_COLOR;
protected static final int CORNER_RADIUS = 15;
private final int VERTICAL_SPACING = 40;
private final Rect rect = ObjectsPool.rect();
private final Paint paint = ObjectsPool.paint();
public CodeBlockSpan(String theme) {
if(theme.equalsIgnoreCase(THEMETYPE.DARK.toString())){
BACKGROUND_COLOR = Color.argb(255, 25, 26, 27);
TEXT_COLOR = Color.WHITE;
}else{
BACKGROUND_COLOR = Color.argb(255, 246, 246, 246);
TEXT_COLOR = Color.BLACK;
}
}
@Override
public void updateMeasureState(TextPaint p) {
apply(p);
}
@Override
public void updateDrawState(TextPaint ds) {
apply(ds);
}
private void apply(TextPaint p) {
final int textColor = TEXT_COLOR;
if (textColor != 0) {
paint.setColor(textColor);
}
paint.setTypeface(Typeface.MONOSPACE);
final int textSize = 0;
if (textSize > 0) {
paint.setTextSize(textSize);
} else {
// calculate default value
paint.setTextSize(paint.getTextSize() * CODE_DEF_TEXT_SIZE_RATIO);
}
}
@Override
public int getLeadingMargin(boolean first) {
return DEFAULT_LEADING_MARGIN;
}
@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) {
paint.setStyle(Paint.Style.FILL);
paint.setColor(BACKGROUND_COLOR);
final int left;
final int right;
if (dir > 0) {
left = x;
right = c.getWidth();
} else {
left = x - c.getWidth();
right = x;
}
rect.set(left, top, right, bottom);
final Spanned txt = (Spanned)text;
final int spanEnd = txt.getSpanEnd(this);
final int spanStart = txt.getSpanStart(this);
// draw rounded corner background
if (start == spanStart) {
c.drawRoundRect(new RectF(rect), CORNER_RADIUS, CORNER_RADIUS, paint);
c.drawRect(new Rect(left, top + CORNER_RADIUS, right, bottom), paint);
}
else if (Math.abs(spanEnd - end) <= 1) {
c.drawRoundRect(new RectF(rect), CORNER_RADIUS, CORNER_RADIUS, paint);
c.drawRect(new Rect(left, top, right, bottom - CORNER_RADIUS), paint);
}
else {
c.drawRect(rect, paint);
}
}
@Override
public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt fm) {
final Spanned txt = (Spanned)text;
final int spanEnd = txt.getSpanEnd(this);
final int spanStart = txt.getSpanStart(this);
// add top spacing to first line
if (start == spanStart) {
fm.ascent -= VERTICAL_SPACING;
fm.top -= VERTICAL_SPACING;
}
// add bottom spacing to last line
if (Math.abs(spanEnd - end) <= 1) {
fm.descent += VERTICAL_SPACING;
fm.bottom += VERTICAL_SPACING;
}
}
}

View File

@ -0,0 +1,33 @@
package io.noties.markwon.span.ext;
import android.graphics.drawable.Drawable;
import android.text.style.DynamicDrawableSpan;
import com.bumptech.glide.load.resource.gif.GifDrawable;
public class GifDrawableSpan extends DynamicDrawableSpan {
private GifDrawable drawable;
private int requiredWidth = 0;
private int requiredHeight = 0;
public GifDrawableSpan(GifDrawable drawable){
this.drawable = drawable;
}
public GifDrawableSpan(GifDrawable drawable, int requiredWidth, int requiredHeight){
this.requiredWidth = requiredWidth;
this.requiredHeight = requiredHeight;
this.drawable = drawable;
}
@Override
public Drawable getDrawable() {
// drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
if (requiredHeight != 0 && requiredWidth != 0) {
drawable.setBounds(0, 0, requiredWidth, requiredHeight);
} else {
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
}
return drawable;
}
}

View File

@ -0,0 +1,48 @@
package io.noties.markwon.span.ext;
import android.text.TextPaint;
import android.text.style.URLSpan;
import android.view.View;
import androidx.annotation.NonNull;
import io.noties.markwon.LinkResolver;
public class HashtagSpan extends URLSpan {
private final String link;
private final LinkResolver resolver;
private final int linkColor;
public HashtagSpan(
@NonNull String link,
@NonNull int linkColor,
@NonNull LinkResolver resolver) {
super(link);
this.link = link;
this.linkColor = linkColor;
this.resolver = resolver;
}
@Override
public void onClick(View widget) {
resolver.resolve(widget, link);
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
applyLinkStyle(ds);
}
private void applyLinkStyle(TextPaint paint){
paint.setUnderlineText(true);
paint.setColor(linkColor);
}
/**
* @since 4.2.0
*/
@NonNull
public String getLink() {
return link;
}
}

View File

@ -0,0 +1,60 @@
package io.noties.markwon.span.ext;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.Layout;
import android.text.Spanned;
import android.text.style.LeadingMarginSpan;
import com.campuswire.android.messenger.model.THEMETYPE;
class NumberedSpan implements LeadingMarginSpan {
private final int NUMBER_GAP = 14;
private final int mIndex;
private final String mTheme;
private final Paint paint = ObjectsPool.paint();
NumberedSpan(int index, String theme) {
mIndex = index;
mTheme = theme;
}
@Override
public int getLeadingMargin(boolean first) {
final String numText = mIndex + ".";
final Rect textBounds = new Rect();
paint.getTextBounds(numText, 0, numText.length(), textBounds);
return textBounds.width() + NUMBER_GAP;
}
@Override
public void drawLeadingMargin(Canvas canvas, Paint paint, int x, int dir, int top, int baseline,
int bottom, CharSequence text, int start, int end, boolean first,
Layout layout) {
// add number to first line
if (((Spanned)text).getSpanStart(this) == start) {
// save previous paint values
final Paint.Style prevStyle = paint.getStyle();
final int prevColor = paint.getColor();
// draw number
paint.setStyle(Paint.Style.FILL);
if(mTheme.equalsIgnoreCase(THEMETYPE.DARK.toString())){
paint.setColor(Color.WHITE);
}else{
paint.setColor(Color.BLACK);
}
final String numText = mIndex + ".";
final Rect textBounds = new Rect();
paint.getTextBounds(numText, 0, numText.length(), textBounds);
final float yVal = (top + bottom + textBounds.height()) / 2f;
canvas.drawText(numText, 0, numText.length(), x, yVal, paint);
// reset modified paint values
paint.setStyle(prevStyle);
paint.setColor(prevColor);
}
}
}

View File

@ -0,0 +1,33 @@
package io.noties.markwon.span.ext;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
abstract class ObjectsPool {
// maybe it's premature optimization, but as all the drawing is done in one thread
// and we apply needed values before actual drawing it's (I assume) safe to reuse some frequently used objects
// if one of the spans need some really specific handling for Paint object (like colorFilters, masks, etc)
// it should instantiate own instance of it
private static final Rect RECT = new Rect();
private static final RectF RECT_F = new RectF();
private static final Paint PAINT = new Paint(Paint.ANTI_ALIAS_FLAG);
static Rect rect() {
return RECT;
}
static RectF rectF() {
return RECT_F;
}
static Paint paint() {
return PAINT;
}
private ObjectsPool() {
}
}

View File

@ -0,0 +1,17 @@
package io.noties.markwon.ext;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}