前言
由于通勤时间较长,在路上总会有时间刷刷文章。稀土掘金就是常用的一个app(这里非广告,哈哈哈)。前段时间,发表了篇文章:# 使用CollapsingToolbarLayout高仿稀土掘金个人中心页,也是跟它相关的。今天再来一篇,不是什么大技术,而是我们常用的自定义view那套东西,只是觉得效果精美,就想自己实现下~先上图:
实现
先分析下效果:
- 字体部分内容高亮
- 高亮部分为平行四边形,而非矩形
实现思路:先绘制浅色字体,再绘制深色字体,不过深色字体只显示平行四边形部分区域。下边直接上代码:
自定义属性
在values目录,创建attrs.xml文件,用于定义属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FlickerText">
<attr name="text" format="string" />
<attr name="text_size" format="dimension" />
<attr name="flick_precent" format="float" />
<attr name="text_normal_color" format="color|reference" />
<attr name="text_flick_color" format="color|reference" />
</declare-styleable>
</resources>
复制代码
这里主要包含了几个属性:
- 显示的文本内容
- 显示的文本字体大小
- 高亮的四边形的宽度比例
- 默认的字体颜色
- 高亮的字体颜色
自定义FlickerView,继承于View
class FlickerText : View {
private var minWidth = 0
private var minHeight = 0
private lateinit var paint: Paint
private var textSize = 120
private var showText: String = ""
private var normalColor = Color.parseColor("#F0F0F2")
private var flickColor = Color.parseColor("#DCDCDC")
private var flickPercent = 0.16f
private var clipLeft = -VERTICALOFFSET
private var path: Path = Path()
constructor(context: Context) : super(context) {
init(null, 0, 0)
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
init(attributeSet, 0, 0)
}
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(
context,
attributeSet,
defStyleAttr
) {
init(attributeSet, defStyleAttr, 0)
}
复制代码
这里给出了自定义属性的默认值
获取配置属性值
private fun init(attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) {
// 获取配置属性
context.theme.obtainStyledAttributes(
attrs,
R.styleable.FlickerText,
0, 0
).apply {
try {
showText = getString(R.styleable.FlickerText_text).toString()
textSize =
getDimensionPixelSize(R.styleable.FlickerText_text_size, textSize)
normalColor = getColor(R.styleable.FlickerText_text_normal_color, normalColor)
flickColor = getColor(R.styleable.FlickerText_text_flick_color, flickColor)
flickPercent = getFloat(R.styleable.FlickerText_flick_precent, flickPercent)
} finally {
recycle()
}
}
// 初始化画笔相关
paint = Paint()
paint.isAntiAlias = true
paint.textSize = textSize.toFloat()
val textBound = Rect()
paint.getTextBounds(showText, 0, showText.length, textBound)
minWidth = textBound.width()
minHeight = textBound.height()
}
复制代码
通过obtainStyledAttributes获取到在xml中配置的自定义属性值。这里还根据设置画笔的字体大小,计算出需要正常显示完整文本需要的宽高
计算View高度
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
var width = 0
var height = 0
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize
} else {
width = min(widthSize, minWidth)
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize
} else {
height = min(heightSize, minHeight)
}
setMeasuredDimension(width, height)
}
复制代码
这里主要做了两层判断:
- 假如mode为EXACTLY,说明指定了具体值,则直接使用
- 假如mode为AT_MOST或UNSPECIFIED,判断父布局提供的大小与上方计算出的显示完整文本需要的大小,取最小值,保证不会超过父布局提供的大小
绘制
这才是显示效果的重点~
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
paint.color = normalColor
canvas?.drawText(showText, 0f, height * 0.5f, paint)
path.reset()
path.moveTo(clipLeft, 0f)
path.lineTo(clipLeft + width * flickPercent, 0f)
path.lineTo(clipLeft + width * flickPercent + VERTICALOFFSET, height.toFloat())
path.lineTo(clipLeft + VERTICALOFFSET, height.toFloat())
paint.color = flickColor
canvas?.clipPath(path)
canvas?.drawText(showText, 0f, height * 0.5f, paint)
clipLeft += 5f
if (clipLeft > width) {
clipLeft = -VERTICALOFFSET
}
invalidate()
}
复制代码
- 这里主要使用了canvas的clipPath函数,该函数会裁剪画布,并根据设置的模式,显示特定效果(这里将先绘制的描述为A,后绘制的描述为B):
- DIFFERENCE:A不同于B的部分显示出来
- REPLACE:显示B的部分
- REVERSE_DIFFERENCE:B中不同于A的部分显示出来
- INTERSECT:A和B的交集
- UNION:A和B的全集
- XOR:全集形状减去交集形状之后的部分
// 查看api,默认使用的是Region.Op.INTERSECT
public boolean clipPath(@NonNull Path path) {
return clipPath(path, Region.Op.INTERSECT);
}
复制代码
- Path的定义,就是组装成一个平行四边形。每次重绘后,需要调用path.reset()清空之前的路径。
- clipLeft自增是为了让高亮部分逐渐往右滚动显示
总结与拓展
总结
其实实现起来,效果很简单,主要就是使用了canvas的clipPath函数。但是可能由于平时少用,所以没有注意到。所以有空还是多看下源码,可以发现些有趣的东西。
拓展
- 这里主要涉及到自定义view的一些知识,官方有相关的一些介绍:官方自定义view教程
- Paint也有个类似的api:setXfermode
Set or clear the transfer mode object. A transfer mode defines how source pixels (generate by a drawing command) are composited with the destination pixels (content of the render target).
Pass null to clear any previous transfer mode. As a convenience, the parameter passed is also returned.
public Xfermode setXfermode(Xfermode xfermode) {
return installXfermode(xfermode);
}
复制代码
可以通过该api实现图像混合模式,PorterDuffXfermode主要包含了以下几种模式:
之前一些抽奖的橡皮擦功能就可以通过这种方式实现。具体的就不多说~
最后,按照惯例附上demo地址:gitee-demo