重写 TextView 实现横向和竖向的跑马灯效果

548 阅读2分钟

需求

  • 一行文字时横向滚动(横向跑马灯效果)
  • 多行文字时竖向滚动(竖向跑马灯效果,非翻页效果)

因为

  1. android自带的跑马灯需要焦点才能滚动,但实际开发中,可能会出现焦点被抢占的情况
  2. 第二种竖向跑马灯效果在网络上没有搜到轮子

所以 决定自己搞一个

效果

图片卡段,实际很丝滑

2.gif

代码

代码不多,直接贴

原理是重写TextView中的onDraw方法,所以有一些原生TextView属性不会再生效

但是现在的效果已经满足我的需求,所以没有继续添加,后续有需要再继续添加 无非是将原生的TextView属性再重新设置下

调用该方法设置文字
setText(text: String, maxLines: Int, isScroll: Boolean) 
package com.cn.markdown.widget

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.text.TextUtils
import android.text.TextUtils.TruncateAt
import android.util.AttributeSet
import androidx.annotation.NonNull
import androidx.appcompat.widget.AppCompatTextView

/**
 * @author : 鹿小柒丶
 * date    : 2023年11月17日14:09:43
 * desc    : 内容可以滚动的 TextView
 */
class ScrollingTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int = 0) : AppCompatTextView(context, attrs, defStyleAttr) {

    companion object {

        /**
         * 移动速度
         */
        private const val SPEED = 1

        /**
         * 横向间距
         */
        private const val PADDING = 50
    }

    private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG)

    /**
     * 文本宽度
     */
    private var textWidth = 0f

    /**
     * 文本高度
     */
    private var textHeight = 0f

    /**
     * 移动偏移量
     */
    private var offset = 0f

    /**
     * 文本内容
     */
    private val textList = ArrayList<String>()

    /**
     * TextView的 ellipsize
     */
    private var mEllipsize: TruncateAt? = null

    /**
     * 最大行数
     */
    private var mMaxLines = -100

    /**
     * 是否需要滚动
     */
    private var isScroll = false

    /**
     * 是否是竖向滚动
     */
    private var isVerticalScrolling: Boolean = false

    /**
     * 是否是横向滚动滚动
     */
    private var isHorizontalScrolling: Boolean = false

    init {
        textPaint.textSize = textSize
        textPaint.color = currentTextColor
        textHeight = getHeight(textPaint)
        mEllipsize = ellipsize
    }

    /**
     * @param text 文本内容
     * @param maxLines 最大行数
     * @param isScroll 是否需要滚动
     */
    fun setText(text: String, maxLines: Int, isScroll: Boolean) {
        this.mMaxLines = maxLines
        this.isScroll = isScroll
        this.offset = 0f
        this.isVerticalScrolling = false
        this.isHorizontalScrolling = false
        this.textList.clear()
        this.ellipsize = if (isScroll) {
            null
        } else {
            mEllipsize
        }
        this.setText(text, BufferType.NORMAL)
        this.maxLines = maxLines
    }

    override fun requestLayout() {
        super.requestLayout()
        //如果设置的最大行数 和 TextView的最大行数相等,才去执行
        if (mMaxLines == maxLines && !TextUtils.isEmpty(text) && isScroll) {
            if (maxLines == 1) {
                //如果是一行,判断是否需要横向滚动
                textWidth = textPaint.measureText(text.toString())
                isHorizontalScrolling = textWidth > width
            } else {
                //如果是一行,判断是否需要竖向滚动,并将每一内容放到list中
                //这里需要放到 post 中执行,否则获取的 lineCount 会错误
                post {
                    isVerticalScrolling = textHeight * lineCount > height
                    if (isVerticalScrolling) {
                        for (index in 0 until lineCount) {
                            val start = layout.getLineStart(index)
                            val end = layout.getLineEnd(index)
                            val mText = text
                                .subSequence(start, end)
                                .toString()
                            textList.add(mText.replace("\n", ""))
                        }
                        textList.add("")
                    }
                    invalidate()
                }
            }
        }
    }

    override fun onDraw(canvas: Canvas) {
        when {
            isVerticalScrolling -> { //竖向滚动
                val totalTextHeight = textHeight * textList.size
                textList.forEachIndexed { index, text ->
                    var y = (offset + textHeight * (index + 1)) % totalTextHeight
                    if (y <= 0) {
                        y += totalTextHeight
                    }
                    canvas.drawText(text, 0f, y, textPaint)
                }
                offset -= SPEED
                postInvalidate()
            }
            isHorizontalScrolling -> { //横向滚动,为了实现跑马灯效果,将同一段文字绘制了两次,实现收尾相连的跑马灯效果
                canvas.drawText(text.toString(), offset, baseline.toFloat(), textPaint)
                canvas.drawText(text.toString(), offset + textWidth + PADDING, baseline.toFloat(), textPaint)

                if (textWidth + offset <= 0) {
                    offset += textWidth + PADDING
                }
                offset -= SPEED
                postInvalidate()
            }
            else -> { //不滚动
                super.onDraw(canvas)
            }
        }
    }

    /**
     * 获取字体绘制一行高度
     * @return float
     */
    private fun getHeight(@NonNull paint: Paint): Float {
        val fm = paint.fontMetrics
        return fm.descent - fm.ascent + fm.leading
    }
}