android自定义view,使用动画实现跑马灯效果

Android Textview实现了跑马灯效果,但是却常常因为各种各样的原因不起作用。

本文实现的是SimpleMarqueeView继承至View,利用ValueAnimator实现的高性能、与TextView体验一致的跑马灯效果。

本文源码已开源,GIT链接
如果你懒得看代码,可以直接使用

//gradle file
implementation 'li.y.z:simplemarqueeviewlib:1.1.0'

!!继承至View,不可以当作普通TextView使用!!
!!继承至View,不可以当作普通TextView使用!!
!!继承至View,不可以当作普通TextView使用!!
!!重要的事情说三遍!!


效果图(录制丢帧、质量差,实际流畅。)

image

实现原理:

修改view的draw方法,绘制两段相同的文本并利用ValueAnimator使两段文本动起来,实现简单的位移效果。

实现思路:

首先我们明确需求,高性能简易跑马灯,模仿系统跑马灯效果,所以我们选择继承View来实现,并设计基础属性。


     //文本宽度
    //文本颜色
    //文本样式 粗体、斜体、粗斜混合
        
        
        
        
    
     //滚动速度
    //滚动间隔
    //两段文本间距
    //文本
    //两端阴影宽度

然后要计算文本宽度,如果比实际显示区域宽,才使用跑马灯效果,否则使用普通显示,直接drawtext即可。

private val textPaintby lazy {
    TextPaint().apply {
        this.color = [email protected]
        this.textSize = [email protected]
        this.typeface = [email protected]
        this.isAntiAlias = true
    }
}
private fun measureTxt() {
    txtWidth= textPaint.measureText(mText).toInt()
    scale= txtWidth/ (width - paddingStart - paddingEnd) + 1
}

然后通过计算得出文本宽度是否超过控件显示宽度,如果宽度超过,则是跑马灯模式,否则为普通文本模式

private fun switchShowMode() {
    showMode= if (txtWidth+ paddingStart + paddingEnd > width) {
        //跑马灯模式
        1
    } else {
        //正常显示
        0
    }
}

注意,计算宽度时一定要等view计算完成后进行,所以我们的代码应该是

view.post {
    measureTxt()
    switchShowMode()
}

准备工作已经完成,下面是具体的动画逻辑

private fun startAnim() {
    stopAnim()
    //为了方便,动画值为文本实际位移值,位移值=文本宽度+两段文本间距
    anim= ValueAnimator.ofInt(0, (txtWidth+ margin).toInt())
    anim?.duration = ((txtWidth+ margin) * speed).toLong()
    anim?.interpolator = LinearInterpolator()
    anim?.repeatCount = 0
    anim?.addUpdateListener {
        animValue= it.animatedValue as Int
        invalidate()
}
    anim?.addListener(object :Animator.AnimatorListener {
        override fun onAnimationRepeat(animation:Animator?) {
}
        override fun onAnimationEnd(animation:Animator?) {
            animValue= 0
            startAnim()
}
        override fun onAnimationCancel(animation:Animator?) {
}
        override fun onAnimationStart(animation:Animator?) {
}
})
//设置动画间隔,否则会一直滚动
    anim?.startDelay = delay
    anim?.start()
}

下面是实际绘制代码

val x= -animValue.toFloat() + paddingStart
canvas?.drawText(mText, x, textSize+ (height - textSize) / 2f - sp2px(1f), textPaint)
val x1= x+ margin+ txtWidth
//这里要注意一下,因为跑马灯是一段文本交替显示,所以我们绘制两段相同的文本实现该效果
canvas?.drawText(mText, x1, textSize+ (height - textSize) / 2f - sp2px(1f), textPaint)

这样我们就实现了一个简单高效的跑马灯啦!阴影、间距、等其他系统效果请查看完整代码

完整代码:

package li.yz.simplemarqueeviewlib

import android.animation.Animator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.graphics.drawable.ColorDrawable
import android.text.TextPaint
import android.util.AttributeSet
import android.view.View
import android.view.animation.LinearInterpolator
import kotlin.math.abs

/**
 * createed by liyuzheng on 2019/7/30 15:06
 */
class SimpleMarqueeView : View {
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init(context, attrs, 0)
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        init(context, attrs, defStyleAttr)
    }

    private fun init(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
        val dm = context.applicationContext.resources.displayMetrics
        density = dm.density
        scaleDensity = dm.scaledDensity

        val a = context.obtainStyledAttributes(
            attrs, R.styleable.SimpleMarqueeView, defStyleAttr, defStyleAttr
        )
        textSize = a.getDimension(R.styleable.SimpleMarqueeView_textSize, sp2px(12f).toFloat())
        textColor = a.getColor(R.styleable.SimpleMarqueeView_textColor, Color.parseColor("#000000"))
        when (a.getInt(R.styleable.SimpleMarqueeView_textStyle, 1)) {
            1 -> typeFace = Typeface.DEFAULT
            2 -> typeFace = Typeface.DEFAULT_BOLD
            3 -> typeFace = Typeface.defaultFromStyle(Typeface.ITALIC)
            4 -> typeFace = Typeface.defaultFromStyle(Typeface.BOLD_ITALIC)
        }
        val text = a.getString(R.styleable.SimpleMarqueeView_text) ?: ""
        shadowWidth = a.getDimension(R.styleable.SimpleMarqueeView_shadow_width, dp2px(14f).toFloat())
        margin = a.getDimension(R.styleable.SimpleMarqueeView_margin_txt, dp2px(133f).toFloat())
        speed = a.getInt(R.styleable.SimpleMarqueeView_speed, 12).toLong()
        delay = a.getInt(R.styleable.SimpleMarqueeView_delay, 1500).toLong()
        a.recycle()
        setText(text)
    }

    private var density: Float = 2f
    private var scaleDensity: Float = 2f
    //font size
    private var textSize = 33f
    //font color
    private var textColor = Color.parseColor("#000000")
    //style
    private var typeFace = Typeface.DEFAULT

    //文本
    private var mText = ""
    //compute text width if txtWidth>width  user marquee
    private var txtWidth = 0
    //shadow,if background is not color , that is not useful
    private var shadowWidth = 0f
    //the system marquee textview is 12L
    private var speed = 12L
    //animation delay
    private var delay = 1500L
    //between two texts margin
    private var margin = 0f
    //0 text 1 marquee
    private var showMode = 0
    private var anim: ValueAnimator? = null
    private var animValue: Int = 0

    private var leftShadow: LinearGradient? = null
    private var rightShadow: LinearGradient? = null

    private var paddingRect: Rect = Rect()
    private val shadowPaint by lazy {
        Paint()
    }

    // if background is not color, it's not useful
    private fun initShadow() {
        if (background is ColorDrawable) {
            val colorD = ColorDrawable((background as? ColorDrawable)?.color ?: 0)
            colorD.alpha = 255
            val sColorInt = colorD.color
            colorD.alpha = 0
            val eColorInt = colorD.color
            if (shadowWidth > 0) {
                leftShadow = LinearGradient(
                    paddingStart.toFloat(),
                    0f,
                    paddingStart.toFloat() + shadowWidth,
                    0f,
                    sColorInt,
                    eColorInt,
                    Shader.TileMode.CLAMP
                )
                rightShadow = LinearGradient(
                    width - paddingEnd.toFloat() - shadowWidth,
                    0f,
                    width - paddingEnd.toFloat(),
                    0f,
                    eColorInt,
                    sColorInt,
                    Shader.TileMode.CLAMP
                )
            }

        }
    }

    private val textPaint by lazy {
        TextPaint().apply {
            this.color = [email protected]
            this.textSize = [email protected]
            this.typeface = [email protected]
            this.isAntiAlias = true
        }
    }

    fun setText(text: String, force: Boolean = false) {
        if (text == mText && !force) return
        this.mText = text
        stopAnim()
        post {
            if (visibility == VISIBLE) {
                initShadow()
                measureTxt()
                switchShowMode()
                show()
            }
        }
    }

    fun getText() = mText

    override fun setVisibility(visibility: Int) {
        super.setVisibility(visibility)
        if (visibility == View.VISIBLE) {
            setText(mText, true)
        } else {
            stopAnim()
        }
    }

    private fun show() {
        animValue = 0
        if (showMode == 0) {
            invalidate()
        } else {
            invalidate()
            startAnim()
        }
    }

    private fun startAnim() {
        stopAnim()
        anim = ValueAnimator.ofInt(0, (txtWidth + margin).toInt())
        anim?.duration = ((txtWidth + margin) * speed).toLong()
        anim?.interpolator = LinearInterpolator()
        anim?.repeatCount = 0
        anim?.addUpdateListener {
            animValue = if (showMode == 0) {
                it.cancel()
                0
            } else {
                it.animatedValue as Int
            }
            invalidate()
        }
        anim?.addListener(object : Animator.AnimatorListener {
            override fun onAnimationRepeat(animation: Animator?) {
            }

            override fun onAnimationEnd(animation: Animator?) {
                show()
            }

            override fun onAnimationCancel(animation: Animator?) {
            }

            override fun onAnimationStart(animation: Animator?) {

            }
        })
        anim?.startDelay = delay
        anim?.start()
    }

    private fun stopAnim() {
        anim?.cancel()
        anim?.removeAllListeners()
        anim = null
        animValue = 0
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        val x = -animValue.toFloat() + paddingStart
        val y = x + margin + txtWidth
        paddingRect.left = paddingStart
        paddingRect.top = paddingTop
        paddingRect.right = width - paddingEnd
        paddingRect.bottom = height - paddingBottom
        canvas?.clipRect(paddingRect)
        canvas?.drawText(mText, x, textSize + (height - textSize) / 2f - sp2px(1f), textPaint)
        if (showMode == 1) {
            canvas?.drawText(mText, y, textSize + (height - textSize) / 2f - sp2px(1f), textPaint)
            if (abs(x) < txtWidth - paddingStart && anim?.isRunning == true) {
                leftShadow?.run {
                    shadowPaint.shader = this
                    canvas?.drawRect(
                        paddingStart.toFloat(),
                        0f,
                        paddingStart + shadowWidth,
                        height.toFloat(),
                        shadowPaint
                    )
                }
            }

            rightShadow?.run {
                shadowPaint.shader = this
                canvas?.drawRect(
                    width - paddingEnd.toFloat() - shadowWidth,
                    0f,
                    width - paddingEnd.toFloat(),
                    height.toFloat(),
                    shadowPaint
                )
            }
        }
    }


    private fun switchShowMode() {
        showMode = if (txtWidth + paddingStart + paddingEnd > width) {
            //跑马灯模式
            1
        } else {
            //正常显示
            0
        }
    }

    //compute txt width
    private fun measureTxt() {
        txtWidth = textPaint.measureText(mText).toInt()
    }


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

    private fun dp2px(dipValue: Float): Int {
        return (dipValue * density + 0.5f).toInt()
    }

    private fun sp2px(spValue: Float): Int {
        return (spValue * scaleDensity + 0.5f).toInt()
    }

    //support height wrap_content
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
        if (heightSpecMode == MeasureSpec.AT_MOST) {
            val tMin = dp2px(3f)
            val pTop = if (paddingTop < tMin) tMin else paddingTop
            val pBottom = if (paddingBottom < tMin) tMin else paddingBottom
            setMeasuredDimension(widthSpecSize, (textSize + pTop + pBottom).toInt())
        }
    }

    //if you want  pause anim,use it
    fun pause() {
        anim?.takeIf {
            it.isRunning
        }?.run {
            pause()
        }
    }

    //if you want resume anim,use it
    fun resume() {
        anim?.run {
            resume()
        }
    }
}

本文源码已开源,GIT链接

你可能感兴趣的:(android自定义view,使用动画实现跑马灯效果)