Android Textview实现了跑马灯效果,但是却常常因为各种各样的原因不起作用。
本文实现的是SimpleMarqueeView继承至View,利用ValueAnimator实现的高性能、与TextView体验一致的跑马灯效果。
本文源码已开源,GIT链接
如果你懒得看代码,可以直接使用
//gradle file
implementation 'li.y.z:simplemarqueeviewlib:1.1.0'
!!继承至View,不可以当作普通TextView使用!!
!!继承至View,不可以当作普通TextView使用!!
!!继承至View,不可以当作普通TextView使用!!
!!重要的事情说三遍!!
效果图(录制丢帧、质量差,实际流畅。)
实现原理:
修改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链接