跑马灯TextView
TextView提供的marquee其实已经能满足大多数人的需求,但是有一种特殊情况,就是在跑马灯滚动的时候,你再setText,会发现滚动动画会重新开始,搜了搜网上好像并没有对这种情况做处理,所以以下提供两种解决方案。
方案一
/**
* 一直滚动的textview
* */
class ContinuousMarqueeTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
private var scrollX = 0f // 当前滚动的偏移量
private var textWidth = 0f // 文本宽度
private var scroller: Scroller? = null
private var isScrolling = false
var spaceWidth = 10f // 文本之间的空白宽度(单位:像素)
init {
isFocusable = true
ellipsize = null // 禁用系统的跑马灯
scroller = Scroller(context, LinearInterpolator())
startScroll()
}
private var viewWidth = 0f // 控件宽度
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w.toFloat()
}
private val bounds = Rect()
private var isBoundsDirty = true
private fun TextPaint.getCachedBounds(text: String): Rect {
if (isBoundsDirty) {
this.getTextBounds(text, 0, text.length, bounds)
isBoundsDirty = false
}
return bounds
}
override fun onDraw(canvas: Canvas) {
val paint: TextPaint = paint
paint.isAntiAlias = true
// 获取文本内容
val text = text.toString()
if (text.isEmpty()) return
textWidth = paint.getCachedBounds(text).width().toFloat()
if (textWidth <= viewWidth) {
isScrolling = false // 停止滚动
val xPos = (viewWidth - textWidth) / 2 // 计算文本居中位置
canvas.drawText(text, xPos, height / 2f + textSize / 2f, paint)
stopScroll()
return
} else {
startScroll()
}
// 计算滚动后的偏移量
scrollX += 2f // 每次滚动的距离
if (scrollX >= textWidth + spaceWidth) {
scrollX = 0f // 滚动完成后重置
}
canvas.drawText(text, -scrollX, height / 2f + textSize / 2f, paint) // 当前文本
canvas.drawText(
text,
-scrollX + textWidth + spaceWidth,
height / 2f + textSize / 2f,
paint
) // 后续文本
if (isScrolling) {
postInvalidateOnAnimation()
}
}
fun updateText(newText: String) {
if (newText != text) {
text = newText
isBoundsDirty = true
}
}
/**
* 开始滚动
*/
private fun startScroll() {
if (!isScrolling) {
isScrolling = true
invalidate()
}
}
/**
* 停止滚动
*/
private fun stopScroll() {
isScrolling = false
}
}
这种方案会受到帧率影响,但是连续效果会好点
方案二
/**
* 持续滚动的textview
* */
class ContinueTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
private var scroller: Scroller? = null
private var viewWidth = 0f // 控件宽度
private val bounds = Rect()
private var isBoundsDirty = true
private var isScrolling = false
private fun TextPaint.getCachedBounds(text: String): Rect {
if (isBoundsDirty) {
this.getTextBounds(text, 0, text.length, bounds)
isBoundsDirty = false
}
return bounds
}
init {
isFocusable = true
ellipsize = null // 禁用系统的跑马灯
paint.isAntiAlias = true
scroller = Scroller(context, LinearInterpolator())
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w.toFloat()
}
/**
* 轮播滚动
* */
private fun startScroll() {
if (viewWidth == 0f) {
// 控件还没初始化完成,不滚动
return
}
if (text.isEmpty()) {
// 没有文本内容,不滚动
return
}
if (checkCanScrolled(text.toString())) {
// 文本宽度小于控件宽度,不滚动
return
}
val textWidth = paint.getCachedBounds(text.toString()).width().toFloat()
if (!isScrolling) {
// 动态调整滚动速度
val pixelsPerSecond = when {
textWidth < viewWidth * 2 -> 250 // 短文本稍快
textWidth > viewWidth * 5 -> 180 // 长文本稍慢
else -> 200 // 默认速度
}
val duration = ((textWidth + viewWidth) / pixelsPerSecond * 1000).toLong()
scroller?.startScroll(
-(viewWidth).toInt(),
0,
textWidth.toInt() + (viewWidth).toInt(),
0,
duration.toInt()
)
scrollTo(textWidth.toInt(), 0)
isScrolling = true
postInvalidateOnAnimation()
}
}
/**
* 更新文字
* */
fun updateText(newText: String) {
if (newText != text) {
text = newText
isBoundsDirty = true
checkCanScrolled(newText)
}
}
/**
* 停止滚动
*/
private fun stopScroll() {
scroller?.abortAnimation()
isScrolling = false
}
override fun computeScroll() {
if (scroller?.computeScrollOffset() == true) {
scrollTo(scroller?.currX ?: 0, 0)
postInvalidateOnAnimation()
} else {
stopScroll()
startScroll()
}
}
private fun checkCanScrolled(newText: String): Boolean {
val textWidth = paint.getCachedBounds(newText).width().toFloat()
if (textWidth <= viewWidth) {
//如果不可以滚动,停止滚动,将文本居中显示
stopScroll()
val xPos = -(viewWidth - textWidth) / 2
scrollTo(xPos.toInt(), 0)
invalidate()
return true
}
return false
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
scroller?.abortAnimation()
scroller = null
}
}
方案三
/**
* 使用 ValueAnimator 实现的跑马灯效果 TextView
*/
class ContinuousValueAnimatorMarqueeTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
private var scrollOffset = 0f // 当前滚动偏移量
private var textWidth = 0f // 文本宽度
private var viewWidth = 0f // 控件宽度
private val bounds = Rect()
private var animator: ValueAnimator? = null
private var canScroll = true // 是否可以滚动
var spaceWidth = 50f // 文本之间的间隔宽度(单位:像素)
init {
isFocusable = true
ellipsize = null // 禁用系统跑马灯效果
startScroll()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w.toFloat()
}
private fun TextPaint.getTextWidth(text: String): Float {
getTextBounds(text, 0, text.length, bounds)
return bounds.width().toFloat()
}
override fun onDraw(canvas: Canvas) {
val paint: TextPaint = paint
paint.isAntiAlias = true
val text = text.toString()
if (text.isEmpty()) return
// 计算文本宽度
textWidth = paint.getTextWidth(text)
if (textWidth <= viewWidth) {
// 如果文本宽度小于控件宽度,不滚动,居中显示
val xPos = (viewWidth - textWidth) / 2
canvas.drawText(text, xPos, height / 2f + textSize / 2f, paint)
return
}
// 计算滚动后的偏移量
scrollOffset += 2f
// 每次滚动的距离
if (scrollOffset >= textWidth + spaceWidth) {
scrollOffset = 0f // 滚动完成后重置
}
// 滚动效果
canvas.drawText(
text,
if (ResourceUtils.isLayoutRtl()) scrollOffset else -scrollOffset,
height / 2f + textSize / 2f,
paint
)
canvas.drawText(
text,
if (ResourceUtils.isLayoutRtl()) -(-scrollOffset + textWidth + spaceWidth) else -scrollOffset + textWidth + spaceWidth,
height / 2f + textSize / 2f,
paint
)
}
/**
* 开始滚动
*/
fun startScroll() {
if (text.isEmpty()) return // 没有文本内容,不需要滚动
textWidth = paint.getTextWidth(text.toString()) // 计算文本宽度
if (textWidth <= viewWidth) return // 如果文本太短,不需要滚动
if (animator?.isRunning == true) return // 动画已经运行中
if (!canScroll) return//不需要滚动
val totalScrollDistance = textWidth + spaceWidth // 总共需要滚动的距离
val pixelsPerSecond = when {
textWidth < viewWidth * 2 -> 250 // 短文本稍快
textWidth > viewWidth * 5 -> 180 // 长文本稍慢
else -> 200 // 默认速度
}
val scrollDuration = ((textWidth + viewWidth) / pixelsPerSecond * 1000).toLong()
animator = ValueAnimator.ofFloat(0f, totalScrollDistance).apply {
duration = scrollDuration
interpolator = LinearInterpolator()
addUpdateListener {
postInvalidateOnAnimation()
}
doOnEnd {
if (canScroll && textWidth > viewWidth) {
startScroll() // 重启动画
}
}
start()
}
}
/**
* 禁止自动滚动
* */
fun stopAutoScroll() {
canScroll = false
stopScroll()
}
/**
* 开始自动滚动
*/
fun startAutoScroll() {
canScroll = true
startScroll()
}
/**
* 停止滚动
*/
private fun stopScroll() {
animator?.cancel()
animator = null
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
stopScroll()
}
/**
* 更新文本内容
*/
fun updateText(newText: String) {
if (newText != text) {
text = newText
stopScroll()
startScroll() // 重启动画
}
}
}
这种不受帧率影响,但是连续效果不如第一种好,可以根据实际情况,适当使用上述两种方案,或者有其他方案可以在底下@我,多谢