Skip to content

Commit 3f2202b

Browse files
committed
[1.7.0-feature] 自定义Span: 1、设置Tag标签 2、文本环绕图片
1 parent 5ea9f80 commit 3f2202b

File tree

4 files changed

+243
-6
lines changed

4 files changed

+243
-6
lines changed

app/src/main/kotlin/org/ninetripods/mq/study/activity/SpanStudyActivity.kt

+20-4
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ import org.ninetripods.mq.study.R
1313
import org.ninetripods.mq.study.kotlin.ktx.dp2px
1414
import org.ninetripods.mq.study.kotlin.ktx.id
1515
import org.ninetripods.mq.study.kotlin.ktx.showToast
16-
import org.ninetripods.mq.study.span.CenterImageSpan
17-
import org.ninetripods.mq.study.span.CustomClickSpan
18-
import org.ninetripods.mq.study.span.RelativeSizeColorSpan
19-
import org.ninetripods.mq.study.span.SpanFactory
16+
import org.ninetripods.mq.study.span.*
2017

2118
class SpanStudyActivity : BaseActivity() {
2219

@@ -38,6 +35,8 @@ class SpanStudyActivity : BaseActivity() {
3835
TITLE_3 +
3936
"一代天骄,成吉思汗,只识弯弓射大雕。\n\n" + "俱往矣,数风流人物,还看今朝。"
4037
private const val SPAN_STR2 = "锄禾日当午,\n\t汗滴禾下土。\n谁知盘中餐,\n\t粒粒皆辛苦。"
38+
private const val SPAN_STR3 =
39+
"悯农锄禾日当午,汗滴禾下土。谁知盘中餐,粒粒皆辛苦。"
4140
const val SEG_1 = "北国风光"
4241
const val SEG_2 = "千里冰封"
4342
const val SEG_3 = "万里雪飘"
@@ -124,6 +123,9 @@ class SpanStudyActivity : BaseActivity() {
124123
//3、影响段落的Span
125124
// processParagraph(spanBuilder)
126125

126+
//4、处理TagSpan
127+
//processCustomSpan()
128+
127129
tvSpan.movementMethod = LinkMovementMethod.getInstance()
128130
tvSpan.setSpannableFactory(SpanFactory())
129131
/**
@@ -438,4 +440,18 @@ class SpanStudyActivity : BaseActivity() {
438440
}
439441
}
440442

443+
private fun processCustomSpan() {
444+
val imgDrawable = ResourcesCompat.getDrawable(resources, R.drawable.icon_flower, null)
445+
val builder = SpannableStringBuilder(SPAN_STR3)
446+
//1、设置Tag
447+
//builder.setSpan(TagSpan(), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
448+
449+
//2、设置文本环绕Span效果
450+
builder.setSpan(
451+
TextAroundSpan(TextAroundSpan.ImgInfo(imgDrawable!!, 90.dp2px(), 90.dp2px()), 4, 100),
452+
0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
453+
)
454+
tvSpan.text = builder
455+
}
456+
441457
}

app/src/main/kotlin/org/ninetripods/mq/study/span/CenterImageSpan.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import java.lang.ref.WeakReference
88
/**
99
* Created by mq on 2023/7/2
1010
*/
11-
class CenterImageSpan(drawable: Drawable, verticalAlignment: Int = ALIGN_CENTER) :
11+
class CenterImageSpan(drawable: Drawable, verticalAlignment: Int = ALIGN_BASELINE) :
1212
ImageSpan(drawable, verticalAlignment) {
1313

1414
private var mDrawableRef: WeakReference<Drawable>? = null
@@ -42,7 +42,8 @@ class CenterImageSpan(drawable: Drawable, verticalAlignment: Int = ALIGN_CENTER)
4242
val imgHeight = d.bounds.height() //图片高度
4343
val textHeight = fm.bottom - fm.top //文字行高度
4444
val halfDiffer = (imgHeight - textHeight) / 2
45-
if (imgHeight > textHeight && verticalAlignment == ALIGN_CENTER) {
45+
if (imgHeight > textHeight) {
46+
//图片大于文字 且居中排版
4647
fm.ascent = fmInt.ascent - halfDiffer
4748
fm.descent = fmInt.descent + halfDiffer
4849
fm.top = fmInt.top - halfDiffer
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package org.ninetripods.mq.study.span
2+
3+
import android.graphics.Canvas
4+
import android.graphics.Color
5+
import android.graphics.Paint
6+
import android.graphics.RectF
7+
import android.text.style.ReplacementSpan
8+
import org.ninetripods.mq.study.kotlin.ktx.dp2px
9+
import org.ninetripods.mq.study.kotlin.ktx.log
10+
import org.ninetripods.mq.study.kotlin.ktx.sp2px
11+
12+
/**
13+
* 自定义Tag Span
14+
* Created by mq on 2023/7/31
15+
*
16+
* @property tagColor tag外框颜色
17+
* @property tagRadius tag圆角半径
18+
* @property tagStrokeWidth tag外框宽度
19+
* @property tagMarginLeft tag外框左侧的margin
20+
* @property tagMarginRight tag外框右侧的margin
21+
* @property tagPadding tag内侧文字padding
22+
* @property txtSize 文字大小
23+
* @property txtColor 文字颜色
24+
*/
25+
class TagSpan(
26+
private val tagColor: Int = Color.RED,
27+
private val tagRadius: Float = 2.dp2px().toFloat(),
28+
private val tagStrokeWidth: Float = 1.dp2px().toFloat(),
29+
private val tagMarginLeft: Float = 0.dp2px().toFloat(),
30+
private val tagMarginRight: Float = 5.dp2px().toFloat(),
31+
private val tagPadding: Float = 2.dp2px().toFloat(),
32+
private val txtSize: Float = 14.sp2px().toFloat(),
33+
private val txtColor: Int = Color.RED,
34+
) : ReplacementSpan() {
35+
36+
private var mSpanWidth = 0 //包含了Span文字左右间距在内的宽度
37+
38+
/**
39+
* 返回Span的宽度。子类可以通过更新Paint.FontMetricsInt的属性来设置Span的高度。
40+
* 如果Span覆盖了整个文本,并且高度没有设置,那么draw(Canvas, CharSequence, int, int, float, int, int, int, Paint)方法将不会调用。
41+
*
42+
* @param paint Paint画笔
43+
* @param text 当前文本
44+
* @param start Span开始索引
45+
* @param end Span结束索引
46+
* @param fm Paint.FontMetricsInt,可能是空
47+
* @return 返回Span的宽度
48+
*/
49+
override fun getSize(
50+
paint: Paint,
51+
text: CharSequence?,
52+
start: Int,
53+
end: Int,
54+
fm: Paint.FontMetricsInt?,
55+
): Int {
56+
if (text.isNullOrEmpty()) return 0
57+
paint.textSize = txtSize
58+
//测量包含了Span文字左右间距在内的宽度
59+
mSpanWidth = (paint.measureText(text, start, end) + getTxtLeftW() + getTxtRightW()).toInt()
60+
log("getSize-> text:$text,start:$start,end:$end,fm:$fm,mSpanWidth:$mSpanWidth")
61+
return mSpanWidth
62+
}
63+
64+
/**
65+
* 将Span绘制到Canvas中
66+
*
67+
* @param canvas Canvas画布
68+
* @param text 当前文本
69+
* @param start Span开始索引
70+
* @param end Span结束索引
71+
* @param x Edge of the replacement closest to the leading margin.
72+
* @param top 行文字显示区域的Top
73+
* @param y Baseline基线
74+
* @param bottom 行文字显示区域的Bottom 当在XML中设置lineSpacingExtra时,这里也会受影响
75+
* @param paint Paint画笔
76+
*/
77+
override fun draw(
78+
canvas: Canvas,
79+
text: CharSequence?,
80+
start: Int,
81+
end: Int,
82+
x: Float,
83+
top: Int,
84+
y: Int,
85+
bottom: Int,
86+
paint: Paint,
87+
) {
88+
log("draw -> text:$text,start:$start,end:$end,x:$x,top:$top,y:$y,bottom:$bottom")
89+
if (text.isNullOrEmpty()) return
90+
paint.run {
91+
color = tagColor
92+
isAntiAlias = true
93+
isDither = true
94+
style = Paint.Style.STROKE
95+
strokeWidth = tagStrokeWidth
96+
}
97+
//文字高度
98+
val txtHeight = paint.fontMetricsInt.descent - paint.fontMetricsInt.ascent
99+
//1、绘制标签
100+
val tagRect = RectF(
101+
x + getTagLeft(), top.toFloat(),
102+
x + mSpanWidth - tagMarginRight, (top + txtHeight).toFloat()
103+
)
104+
canvas.drawRoundRect(tagRect, tagRadius, tagRadius, paint)
105+
106+
//2、绘制文字
107+
paint.run {
108+
color = txtColor
109+
style = Paint.Style.FILL
110+
}
111+
// 计算Baseline绘制的Y坐标 ,计算方式:画布高度的一半 - 文字总高度的一半
112+
val baseY = tagRect.height() / 2 - (paint.descent() + paint.ascent()) / 2
113+
//绘制标签内文字
114+
canvas.drawText(text, start, end, x + getTxtLeftW(), baseY, paint)
115+
}
116+
117+
private fun getTagLeft(): Float {
118+
return tagMarginLeft + tagStrokeWidth
119+
}
120+
121+
/**
122+
* Span文字左侧所有的间距
123+
*/
124+
private fun getTxtLeftW(): Float {
125+
return tagPadding + tagMarginLeft + tagStrokeWidth
126+
}
127+
128+
/**
129+
* Span文字右侧所有的间距
130+
*/
131+
private fun getTxtRightW(): Float {
132+
return tagPadding + tagMarginRight + tagStrokeWidth
133+
}
134+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package org.ninetripods.mq.study.span
2+
3+
import android.graphics.Canvas
4+
import android.graphics.Paint
5+
import android.graphics.drawable.Drawable
6+
import android.text.Layout
7+
import android.text.style.LeadingMarginSpan.LeadingMarginSpan2
8+
import org.ninetripods.mq.study.kotlin.ktx.dp2px
9+
10+
11+
/**
12+
* Created by mq on 2023/8/1
13+
* @param lineCount 行数
14+
* @param mFirst 段落前N行margin 单位dp
15+
* @param mRest 段落剩余行margin 单位dp
16+
*/
17+
class TextAroundSpan(
18+
private var imgInfo: ImgInfo,
19+
private val lineCount: Int,
20+
private val mFirst: Int,
21+
private val mRest: Int = 0,
22+
) :
23+
LeadingMarginSpan2 {
24+
25+
26+
/**
27+
* 段落缩进的行数
28+
*/
29+
override fun getLeadingMarginLineCount(): Int = lineCount
30+
31+
/**
32+
* @param first true作用于段落中前N行(N为getLeadingMarginLineCount()中返回的值),否则作用于段落剩余行
33+
*/
34+
override fun getLeadingMargin(first: Boolean): Int =
35+
if (first) mFirst.dp2px() else mRest.dp2px()
36+
37+
38+
/**
39+
* 绘制页边距(leading margin)。在{@link #getLeadingMargin(boolean)}返回值调整页边距之前调用。
40+
*
41+
* @param canvas the canvas
42+
* @param paint the paint. The this should be left unchanged on exit.
43+
* @param x the current position of the margin
44+
* @param dir the base direction of the paragraph; if negative, the margin
45+
* is to the right of the text, otherwise it is to the left.
46+
* @param top the top of the line
47+
* @param baseline the baseline of the line
48+
* @param bottom the bottom of the line
49+
* @param text the text
50+
* @param start the start of the line
51+
* @param end the end of the line
52+
* @param first true if this is the first line of its paragraph
53+
* @param layout the layout containing this line
54+
*/
55+
override fun drawLeadingMargin(
56+
canvas: Canvas?,
57+
paint: Paint?,
58+
x: Int,
59+
dir: Int,
60+
top: Int,
61+
baseline: Int,
62+
bottom: Int,
63+
text: CharSequence?,
64+
start: Int,
65+
end: Int,
66+
first: Boolean,
67+
layout: Layout?,
68+
) {
69+
if (canvas == null || paint == null) return
70+
val drawable: Drawable = imgInfo.drawable
71+
canvas.save()
72+
drawable.setBounds(0, 0, imgInfo.width, imgInfo.height)
73+
canvas.translate(imgInfo.dx, imgInfo.dy)
74+
drawable.draw(canvas)
75+
canvas.restore()
76+
}
77+
78+
data class ImgInfo(
79+
val drawable: Drawable,
80+
val width: Int,
81+
val height: Int,
82+
val dx: Float = 1.dp2px().toFloat(),
83+
val dy: Float = 2.dp2px().toFloat(),
84+
)
85+
86+
}

0 commit comments

Comments
 (0)