自定義view高仿稀土掘金loading閃動字型效果

語言: CN / TW / HK

前言

由於通勤時間較長,在路上總會有時間刷刷文章。稀土掘金就是常用的一個app(這裡非廣告,哈哈哈)。前段時間,發表了篇文章:# 使用CollapsingToolbarLayout高仿稀土掘金個人中心頁,也是跟它相關的。今天再來一篇,不是什麼大技術,而是我們常用的自定義view那套東西,只是覺得效果精美,就想自己實現下~先上圖:

效果圖.gif

實現

先分析下效果: - 字型部分內容高亮 - 高亮部分為平行四邊形,而非矩形 實現思路:先繪製淺色字型,再繪製深色字型,不過深色字型只顯示平行四邊形部分割槽域。下邊直接上程式碼:

自定義屬性

在values目錄,建立attrs.xml檔案,用於定義屬性 ```

``` 這裡主要包含了幾個屬性: - 顯示的文字內容 - 顯示的文字字型大小 - 高亮的四邊形的寬度比例 - 預設的字型顏色 - 高亮的字型顏色

自定義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); } ``` 2. Path的定義,就是組裝成一個平行四邊形。每次重繪後,需要呼叫path.reset()清空之前的路徑。 3. 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