Android自定义MarqueeView 实现跑马灯效果

自定义MarqueeView 实现跑马灯效果,可根据自定义速度、暂停和继续播放

import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.text.TextPaint
import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue
import android.view.View
import android.view.animation.LinearInterpolator
import androidx.annotation.ColorInt
import com.example.flutter_tap.R

/**
 * Created by wsm on 2025/8/18
 * <p>自定义跑马灯View
 */
class MarqueeView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private var TAG = "MarqueeView"
    private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
    private var text = ""
    private var offsetX = 0f
    private var animator: ValueAnimator? = null
    private var isPaused = false
    private var remainingDuration = 0L  // 剩余播放时间
    private var pausedOffsetX = 0f // 记录暂停时的位置

    init {
        // 解析XML属性
        attrs?.let {
            val typedArray = context.obtainStyledAttributes(
                it,
                R.styleable.MarqueeView,
                defStyleAttr,
                0
            )

            // 字体大小(默认16sp)
            textPaint.textSize = typedArray.getDimensionPixelSize(
                R.styleable.MarqueeView_android_textSize,
                16.spToPx().toInt()
            ).toFloat()

            // 字体颜色(默认黑色)
            textPaint.color = typedArray.getColor(
                R.styleable.MarqueeView_android_textColor,
                Color.BLACK
            )
            typedArray.recycle()
        }
    }

    /**
     * 设置文本
     */
    fun setText(text: String) {
        this.text = text
        requestLayout()
        invalidate()
    }

    /**
     * 设置字体类型(Typeface)
     */
    fun setTypeface(typeface: Typeface) {
        textPaint.typeface = typeface
        invalidate()
    }

    /**
     * 设置字体大小(单位:sp)
     */
    fun setTextSize(spSize: Int) {
        textPaint.textSize = spSize.spToPx()
        invalidate()
    }

    /**
     * 动态设置字体颜色
     */
    fun setTextColor(@ColorInt color: Int) {
        textPaint.color = color
        invalidate()
    }

    /** 开始滚动
     * @param durationMs 滚动持续时间(毫秒)值越小速度越快
     * @param startX 滚动起始位置(默认从右侧边界外开始)
     */
    fun startMarquee(durationMs: Long, startX: Float? = null) {
        stopMarquee() // 停止之前的动画

        val textWidth = textPaint.measureText(text)
        val viewWidth = width.toFloat()

        Log.d(TAG, "startMarquee: ")

        // 从右侧边界外开始(startX = viewWidth)
        // 滚动到左侧边界外结束(endX = -textWidth)
        animator = ValueAnimator.ofFloat(startX ?: viewWidth, -textWidth).apply {
            duration = durationMs
            interpolator = LinearInterpolator() // 匀速滚动
            addUpdateListener { animation ->
                offsetX = animation.animatedValue as Float
                invalidate()
            }
            start()
        }
        isPaused = false
    }

    /**
     * 暂停滚动
     */
    fun pause() {
        Log.d(TAG, "pause: ")
        if (animator?.isRunning == true) {
            pausedOffsetX = offsetX // 记录当前位置
            remainingDuration = animator?.duration?.minus(animator?.currentPlayTime ?: 0) ?: 0
            animator?.cancel()
            isPaused = true
        }
    }

    /**
     *  继续滚动
     */
    fun resume() {
        Log.d(TAG, "resume: ")
        if (isPaused && remainingDuration > 0) {
            startMarquee(remainingDuration, pausedOffsetX)
        }
    }

    /**
     *  停止滚动(完全重置)
     */
    fun stopMarquee() {
        Log.d(TAG, "stopMarquee: ")
        animator?.cancel()
        animator = null
        isPaused = false
        remainingDuration = 0
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 绘制文本(y坐标居中)
        canvas.drawText(text, offsetX, height / 2f + textPaint.textSize / 3, textPaint)
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        stopMarquee()
    }

    /**
     * sp转px的扩展函数
     */
    private fun Int.spToPx(): Float {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP,
            this.toFloat(),
            context.resources.displayMetrics
        )
    }
}

res/values/attrs.xml文件下

    <!--跑马灯-->
    <declare-styleable name="MarqueeView">
        <!-- 字体大小(单位:sp) -->
        <attr name="android:textSize" />
        <!-- 字体颜色 -->
        <attr name="android:textColor" />
    </declare-styleable>

布局文件中引入自定义文件

			<com.example.view.MarqueeView
                android:id="@+id/marqueeView"
                android:layout_width="match_parent"
                android:layout_height="20dp"
                android:layout_marginStart="45dp"
                android:layout_marginEnd="45dp"
                android:textColor="#ff191919"
                android:textSize="16dp" />

代码中实现

        marqueeView = findViewById(R.id.marqueeView);
        marqueeView.setText("这是一段实现跑马灯效果的文字,可控制播放速度...");
        marqueeView.startMarquee(5000L, null);//5秒播放
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值