自定义view高仿稀土掘金loading闪动字体效果

2,048 阅读4分钟

前言

由于通勤时间较长,在路上总会有时间刷刷文章。稀土掘金就是常用的一个app(这里非广告,哈哈哈)。前段时间,发表了篇文章:# 使用CollapsingToolbarLayout高仿稀土掘金个人中心页,也是跟它相关的。今天再来一篇,不是什么大技术,而是我们常用的自定义view那套东西,只是觉得效果精美,就想自己实现下~先上图:

效果图.gif

实现

先分析下效果:

  • 字体部分内容高亮
  • 高亮部分为平行四边形,而非矩形 实现思路:先绘制浅色字体,再绘制深色字体,不过深色字体只显示平行四边形部分区域。下边直接上代码:

自定义属性

在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()
}
  1. 这里主要使用了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);
}
  1. Path的定义,就是组装成一个平行四边形。每次重绘后,需要调用path.reset()清空之前的路径。
  2. clipLeft自增是为了让高亮部分逐渐往右滚动显示

总结与拓展

总结

其实实现起来,效果很简单,主要就是使用了canvas的clipPath函数。但是可能由于平时少用,所以没有注意到。所以有空还是多看下源码,可以发现些有趣的东西。

拓展

  1. 这里主要涉及到自定义view的一些知识,官方有相关的一些介绍:官方自定义view教程
  2. 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主要包含了以下几种模式:

mode.jpg

之前一些抽奖的橡皮擦功能就可以通过这种方式实现。具体的就不多说~

最后,按照惯例附上demo地址:gitee-demo