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使用!!
!!重要的事情说三遍!!

<li.yz.simplemarqueeviewlib.SimpleMarqueeView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/text1"
        android:layout_marginTop="100dp"
        android:background="#000000"
        app:text="Hello World! This is a Simple Marquee View. You can use it instead of textviewmarquee"
//下面的参数可选
        app:textColor="@color/colorPrimary"
        app:delay="1500"
        app:margin_txt="133dp"
        app:textSize="12sp"
        app:textColor="@color/colorPrimary"
        app:shadow_width="12dp"
        app:speed="12"
        app:textStyle="bold"
/>

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

image

实现原理:

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

实现思路:

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

<declare-styleable name="SimpleMarqueeView">
    <attr name="textSize" format="dimension"/> //文本宽度
    <attr name="textColor" format="reference|color"/>//文本颜色
    <attr name="textStyle" format="enum">//文本样式 粗体、斜体、粗斜混合
        <enum name="normal" value="1"/>
        <enum name="bold" value="2"/>
        <enum name="italic" value="3"/>
        <enum name="bold_italic" value="4"/>
    </attr>
    <attr name="speed" format="integer"/> //滚动速度
    <attr name="delay" format="integer"/>//滚动间隔
    <attr name="margin_txt" format="dimension"/>//两段文本间距
    <attr name="text" format="string"/>//文本
    <attr name="shadow_width" format="dimension"/>//两端阴影宽度
</declare-styleable>

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

private val textPaintby lazy {
    TextPaint().apply {
        this.color = this@SimpleMarqueeView.textColor
        this.textSize = this@SimpleMarqueeView.textSize
        this.typeface = this@SimpleMarqueeView.typeFace
        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 = this@SimpleMarqueeView.textColor
            this.textSize = this@SimpleMarqueeView.textSize
            this.typeface = this@SimpleMarqueeView.typeFace
            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链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354