class MarqueeView(context: Context, attrs: AttributeSet) : View(context, attrs) { private var msg: String = ""//绘制的文字 private var speed: Int = 1//文字速度 private val duration = 5L//绘制间隔 private val textColor by lazy { Color.BLACK } private var textSize = 12f private val paint by lazy { val p = TextPaint(Paint.ANTI_ALIAS_FLAG) p.style = Paint.Style.FILL p.color = textColor val scale = resources.displayMetrics.density p.textSize = textSize * scale + 0.5f p } private val rect by lazy { Rect() } //协程执行文字的跑马灯效果 private var task: Job? = null //堆代码 duidaima.com //文字的位置 private var xPos = 0f override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) log(msg) canvas?.drawText(msg, xPos, height / 2f + getTextHeight() / 2, paint) } private fun updateXPosition() { if (xPos < -getTextWidth()) { xPos = width.toFloat() } } private fun getTextWidth(): Int { if (msg.isEmpty()) return 0 paint.getTextBounds(msg, 0, msg.length, rect) return rect.width() } private fun getTextHeight(): Int { if (msg.isEmpty()) return 0 paint.getTextBounds(msg, 0, msg.length, rect) return rect.height() } fun startWithContent(msg: String): MarqueeView { if (msg.isEmpty()) return this this.msg = msg startRoll() return this } private fun startRoll() { if (task == null) { task = CoroutineScope(Dispatchers.IO).launch { while (isActive) { kotlin.runCatching { delay(duration) xPos -= speed updateXPosition() postInvalidate() } } } } } fun stop(): MarqueeView { task?.cancel() task = null return this } fun reStart(): MarqueeView { if (msg.isEmpty()) return this startRoll() return this } private fun log(msg:String) { Log.e("TAG", msg) } }一个完整的自定义View,算上import的,差不多100行不到。
canvas?.drawText(msg, xPos, height / 2f + getTextHeight() / 2, paint)第一个参数是跑马灯的内容,第二个参数是文字绘制的X轴坐标,第三个参数是文字绘制的Y轴坐标,第四个参数是画笔。只要改变绘制起始位置的X轴坐标,就能实现文字的位移。假设msg="abcdefg",当xPos==”ab的宽度“的时候,绘制效果如下,
private fun updateXPosition() { if (xPos < -getTextWidth()) { xPos = width.toFloat() } }调用
<com.testdemo.MarqueeView android:id="@+id/marquee" android:layout_width="200dp" android:layout_height="40dp" android:background="@color/c_green" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />activity中的代码
val marqueeView = findViewById<MarqueeView>(R.id.marquee) marqueeView.startWithContent("abcdefg")//测试文字:各方人员请注意,接下来是跑马灯,跑马灯,跑马灯要来了!
...省略代码... private var xPos1 = 0f private val space by lazy { val s = "aaaaaaaa"//8个空格作为衔接 paint.getTextBounds(s, 0, s.length, rect) rect.width() } ...省略代码... override fun onDraw(canvas: Canvas?) { ...省略代码... canvas?.drawText(msg, xPos1, height / 2f + getTextHeight() / 2, paint) } ...省略代码... /** * 更新绘制的位置,实现循环 * 第二个text的位置是在第一个位置后面的8个空格后 */ private fun updateXPosition() { val textWidth = getTextWidth() if (xPos < -textWidth) { xPos = xPos1 + textWidth + space } if (xPos > -textWidth && xPos < width - textWidth) { xPos1 = xPos + space + textWidth } else { xPos1 -= speed } } ...省略代码...
gif就不放了,你们可以直接贴代码。用示例文字"各方人员请注意,接下来是跑马灯,跑马灯,跑马灯要来了!"运行起来没问题。
这就好了?不,不!要知道,此时,文字的总长度是大于自定义View的width的,假如文字的长度小于width的话,就又要考虑不同的情形了。有点麻烦,updateXPosition()函数内的逻辑就复杂起来了。(内心独白:我是一个容易放弃的人,这个就算了,还是让文字身后留出一个自定义view的宽度的空白吧)
还有,假如我想要滚动的不是一个字符串,而是字符串列表List,那怎么办?class MarqueeView(context: Context, attrs: AttributeSet) : View(context, attrs) { private var msg: String = ""//绘制的文字 private var speed: Int = 1//文字速度 private var duration = 5L//绘制间隔 private var textColor = Color.BLACK private var textSize = 12f private val scale by lazy { resources.displayMetrics.density } private val paint by lazy { val p = TextPaint(Paint.ANTI_ALIAS_FLAG) p.style = Paint.Style.FILL p.color = textColor p.textSize = textSize * scale + 0.5f p } private val rect by lazy { Rect() } //协程执行文字的跑马灯效果 private var task: Job? = null // 堆代码 duidaima.com //文字的位置 private var xPos = 0f private val dataList by lazy { ArrayList<String>() } private var dataPos = 0 private var showList = false//设置标记位,判断是否显示list override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) canvas?.drawText(msg, xPos, height / 2f + getTextHeight() / 2, paint) } private fun updateXPosition() { val textWidth = getTextWidth() if (xPos < -textWidth) { xPos = width.toFloat() } } private fun getTextWidth(): Int { if (msg.isEmpty()) return 0 paint.getTextBounds(msg, 0, msg.length, rect) return rect.width() } private fun getTextHeight(): Int { if (msg.isEmpty()) return 0 paint.getTextBounds(msg, 0, msg.length, rect) return rect.height() } fun startWithContent(msg: String): MarqueeView { if (msg.isEmpty()) return this this.msg = msg startRoll() return this } private fun startRoll() { if (task == null) { task = CoroutineScope(Dispatchers.IO).launch { while (isActive) { kotlin.runCatching { delay(duration) xPos -= speed updateXPosition() updateMsg() postInvalidate() } } } } } fun stop(): MarqueeView { task?.cancel() task = null return this } fun reStart(): MarqueeView { if (msg.isEmpty()) return this startRoll() return this } fun setSpeed(speed: Int): MarqueeView { if (speed == 0) return this this.speed = speed return this } fun setDuration(duration: Long): MarqueeView { if (duration == 0L) return this this.duration = duration return this } fun setTextColor(@ColorInt textColor: Int): MarqueeView { this.textColor = textColor paint.color = textColor return this } fun setTextSize(textSize: Float): MarqueeView { if (textSize < 10f) return this paint.textSize = textSize * scale + 0.5f return this } fun startWithList(data: List<String>) : MarqueeView{ showList = true dataList.clear() dataList.addAll(data) if (dataList.isEmpty()) return this startRoll() dataPos = 0 return this } private fun updateMsg() { if (!showList || dataList.isEmpty() || xPos <= (width - speed)) return msg = dataList[dataPos++ % dataList.size] } }