自定义Span
前面的文章介绍了一些常见 Span
的运用场景及其运用示例,本文持续来学习自定义Span
。那么,体系现已供给许多品种的Span了,为什么还要自定义?
- 自定义
Span
能够依据详细需求完成更多样化的文本样式、交互作用和文本布局。 - 当内置的
Span
类无法满足需求时,或许期望完成更定制化的作用时,能够考虑自定义Span
。
既然要自定义Span
,就要考虑父类用哪个适宜。
- 在
字符级别
影响文本 -> CharacterStyle - 在
阶段级别
影响文本 -> ParagraphStyle - 影响
文本外观
-> UpdateAppearance - 影响
文本丈量尺寸
-> UpdateLayout
其间
-
CharacterStyle
中的updateDrawState(TextPaint tp)
本质上是改变画笔TextPaint
的特点; -
ReplacementSpan
及其扩展类DynamicDrawableSpan
不满足于只修改TextPaint
画笔特点,而是运用圈定Rect
制作区域,然后自行制作这个区域的作用。
不过大部分场景下,咱们不必继承到这么深的父类,挑选适宜的已有Span
进行扩展是一个不错的挑选。
题外话:其实能够类比下自定义View
,咱们不必每次都去继承 View
或许 ViewGroup
,挑选一个适宜已有的父类(如横向排版时能够考虑LinearLayout
作为基类)会让整个进程变得简单。
假如体系供给的Span
根本契合需求,只需求细微调整,此刻父类就挑选已有的Span
即可;假如体系供给的Span
不契合需求,能够考虑经过继承ReplacementSpan
来完成。
下面对两种场景别离给出详细示例。
FontMetricsInt 必知必会
在学习自定义 Span
之前,有必要再复习下FontMetrics
,该类描绘了 Text
文本的要害指标信息:
public static class FontMetrics {
public float top;
public float ascent;
public float descent;
public float bottom;
public float leading;
}
FontMetricsInt
跟 FontMetrics
意义相同,仅仅将类中成员变量都改为int
润饰了:
public static class FontMetricsInt {
public int top;
public int ascent;
public int descent;
public int bottom;
public int leading;
}
咱们来看下类中的成员变量都是什么意义。
-
Baseline
是基线,在Android
中,文字的制作都是从Baseline
处开端的,Baseline
往上至字符“最高处”的间隔咱们称之为ascent
(上斜度),Baseline
往下至字符“最低处”的间隔咱们称之为descent
(下斜度); -
leading
(行距离)则表明上一行字符的descent
到该行字符的ascent
之间的间隔; -
top
和bottom
文档描绘地很模糊,其实这儿咱们能够借鉴一下TextView
对文本的制作,TextView
在制作文本的时分总会在文本的最外层留出一些内边距,为什么要这样做?因为TextView
在制作文本的时分考虑到了相似读音符号,下图中的A
上面的符号便是一个拉丁文的相似读音符号的东西:
top
的意思其实便是除了Baseline
到字符顶端的间隔外还应该包含这些符号的高度,bottom
的意思也是相同。
一般情况下咱们极少运用到相似的符号所以往往会忽略掉这些符号的存在,可是Android
依然会在制作文本的时分在文本外层留出必定的边距,这便是为什么top
和bottom
总会比ascent
和descent
大一点的原因。而在TextView
中咱们能够经过xml
设置其特点android:includeFontPadding="false"
去掉必定的边距值可是不能完全去掉。
详细拜见:Android深化理解文字制作:FontMetrics字体丈量及其TextPaint介绍
示例一: 文本盘绕图片
分析上面 UI
作用:阶段的前几行进行缩进,余下行不再缩进,在缩进的空间里制作一张图。看到这儿,根本能想到用哪个父类了,没错,便是LeadingMarginSpan2
。
/**
* @param lineCount 行数
* @param mFirst 阶段前N行margin 单位dp
* @param mRest 阶段剩下行margin 单位dp
*/
class TextAroundSpan(
private var imgInfo: ImgInfo,
private val lineCount: Int,
private val mFirst: Int,
private val mRest: Int = 0,
) : LeadingMarginSpan2 {
/**
* 阶段缩进的行数
*/
override fun getLeadingMarginLineCount(): Int = lineCount
/**
* @param first true作用于阶段中前N行(N为getLeadingMarginLineCount()中回来的值),不然作用于阶段剩下行
*/
override fun getLeadingMargin(first: Boolean): Int =
if (first) mFirst.dp2px() else mRest.dp2px()
/**
* 制作页边距(leading margin)。在{@link #getLeadingMargin(boolean)}回来值调整页边距之前调用。
*/
override fun drawLeadingMargin(
canvas: Canvas?,
paint: Paint?,
x: Int,
dir: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence?,
start: Int,
end: Int,
first: Boolean,
layout: Layout?,
) {
if (canvas == null || paint == null) return
val drawable: Drawable = imgInfo.drawable
canvas.save()
drawable.setBounds(0, 0, imgInfo.width, imgInfo.height)
canvas.translate(imgInfo.dx, imgInfo.dy)
drawable.draw(canvas)
canvas.restore()
}
data class ImgInfo(
val drawable: Drawable,
val width: Int,
val height: Int,
val dx: Float = 1.dp2px().toFloat(),
val dy: Float = 2.dp2px().toFloat(),
)
}
- getLeadingMarginLineCount:阶段缩进的行数;
-
getLeadingMargin(boolean):
true
作用于阶段中前N行(N为getLeadingMarginLineCount()
中回来的值),不然作用于阶段剩下行; -
drawLeadingMargin:制作页边距(
leading margin
),用于制作空出来的margin
,在getLeadingMargin(boolean)
回来值调整页边距之前调用。
运用它:
private const val SPAN_STR =
"悯农锄禾日当午,汗滴禾下土。谁知盘中餐,粒粒皆辛苦。锄禾日当午,汗滴禾下土。谁知盘中餐,粒粒皆辛苦。锄禾日当午,汗滴禾下土。谁知盘中餐,粒粒皆辛苦。锄禾日当午,汗滴禾下土。谁知盘中餐,粒粒皆辛苦。"
private fun processTagSpan() {
val imgDrawable = ResourcesCompat.getDrawable(resources, R.drawable.icon_flower, null)
val builder = SpannableStringBuilder(SPAN_STR)
builder.setSpan(TextAroundSpan(TextAroundSpan.ImgInfo(imgDrawable!!, 90.dp2px(), 90.dp2px()), 4, 100), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
tvSpan.text = builder
}
执行结果便是上面的作用图,能够看到经过继承已有的Span
完成了咱们想要的作用。
示例二: 文本内部打Tag标签
现有的 Span
无法满足需求,考虑运用ReplacementSpan
来完成上述作用,ReplacementSpan
本身是个抽象类,需求完成内部的 getSize
和 draw
两个方法:
-
getSize:回来当时
Span
需求的宽度。子类能够经过更新Paint.FontMetricsInt
的特点来设置Span
的高度。假如Span
覆盖了整个文本,而且高度没有设置,那么draw
方法将不会调用。 -
draw:将
Span
制作到Canvas
中,有了Canvas
和Paint
后,就能够制作咱们想要的作用了。
下面是详细完成代码:
/**
* 自定义Tag Span
*
* @property tagColor tag外框色彩
* @property tagRadius tag圆角半径
* @property tagStrokeWidth tag外框宽度
* @property tagMarginLeft tag外框左边的margin
* @property tagMarginRight tag外框右侧的margin
* @property tagPadding tag内侧文字padding
* @property txtSize 文字大小
* @property txtColor 文字色彩
*/
class TagSpan(
private val tagColor: Int = Color.RED,
private val tagRadius: Float = 2.dp2px().toFloat(),
private val tagStrokeWidth: Float = 1.dp2px().toFloat(),
private val tagMarginLeft: Float = 0.dp2px().toFloat(),
private val tagMarginRight: Float = 5.dp2px().toFloat(),
private val tagPadding: Float = 2.dp2px().toFloat(),
private val txtSize: Float = 14.sp2px().toFloat(),
private val txtColor: Int = Color.RED,
) : ReplacementSpan() {
private var mSpanWidth = 0 //包含了Span文字左右距离在内的宽度
/**
* 回来Span的宽度。子类能够经过更新Paint.FontMetricsInt的特点来设置Span的高度。
* 假如Span覆盖了整个文本,而且高度没有设置,那么draw(Canvas, CharSequence, int, int, float, int, int, int, Paint)方法将不会调用。
*
* @param paint Paint画笔
* @param text 当时文本
* @param start Span开端索引
* @param end Span完毕索引
* @param fm Paint.FontMetricsInt,可能是空
* @return 回来Span的宽度
*/
override fun getSize(
paint: Paint,
text: CharSequence?,
start: Int,
end: Int,
fm: Paint.FontMetricsInt?,
): Int {
if (text.isNullOrEmpty()) return 0
paint.textSize = txtSize
//丈量包含了Span文字左右距离在内的宽度
mSpanWidth = (paint.measureText(text, start, end) + getTxtLeftW() + getTxtRightW()).toInt()
return mSpanWidth
}
/**
* 将Span制作到Canvas中
*
* @param canvas Canvas画布
* @param text 当时文本
* @param start Span开端索引
* @param end Span完毕索引
* @param x Edge of the replacement closest to the leading margin.
* @param top 行文字显现区域的Top
* @param y Baseline基线
* @param bottom 行文字显现区域的Bottom 当在XML中设置lineSpacingExtra时,这儿也会受影响
* @param paint Paint画笔
*/
override fun draw(
canvas: Canvas,
text: CharSequence?,
start: Int,
end: Int,
x: Float,
top: Int,
y: Int,
bottom: Int,
paint: Paint,
) {
if (text.isNullOrEmpty()) return
paint.run {
color = tagColor
isAntiAlias = true
isDither = true
style = Paint.Style.STROKE
strokeWidth = tagStrokeWidth
}
//文字高度
val txtHeight = paint.fontMetricsInt.descent - paint.fontMetricsInt.ascent
//1、制作标签
val tagRect = RectF(
x + getTagLeft(), top.toFloat(),
x + mSpanWidth - tagMarginRight, (top + txtHeight).toFloat()
)
canvas.drawRoundRect(tagRect, tagRadius, tagRadius, paint)
//2、制作文字
paint.run {
color = txtColor
style = Paint.Style.FILL
}
// 计算Baseline制作的Y坐标 ,计算方法:画布高度的一半 - 文字总高度的一半
val baseY = tagRect.height() / 2 - (paint.descent() + paint.ascent()) / 2
//制作标签内文字
canvas.drawText(text, start, end, x + getTxtLeftW(), baseY, paint)
}
private fun getTagLeft(): Float {
return tagMarginLeft + tagStrokeWidth
}
/**
* Span文字左边所有的距离
*/
private fun getTxtLeftW(): Float {
return tagPadding + tagMarginLeft + tagStrokeWidth
}
/**
* Span文字右侧所有的距离
*/
private fun getTxtRightW(): Float {
return tagPadding + tagMarginRight + tagStrokeWidth
}
}
主要思路:
- 首先在
getSize
中经过paint.measureText()
来获取Span
文字的宽度,注意还要加上Span
文字左右的padding
和margin
; - 接着在
draw
中将Span
制作到Canvas
中,经过文字的宽度和左右padding
来确定Tag
边框的规模并制作出来。确定了Tag
边框的规模,找到边框中心然后持续制作文字即可。
Tips:在运用组合Span
时,假如在ReplacementSpan
中改变了Span
的宽度,需求最早设置 ReplacementSpan
,再设置其它Span
,防止呈现位置紊乱问题。