feature: MarqueeTypeView

This commit is contained in:
cgspine
2021-01-21 14:02:23 +08:00
parent 1744053908
commit be066fcbc3
12 changed files with 635 additions and 283 deletions
@@ -22,102 +22,64 @@ import android.text.TextUtils.TruncateAt
import com.qmuiteam.qmui.type.element.*
import java.util.*
class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
private var mMaxLines = Int.MAX_VALUE
private var mEllipsize: TruncateAt? = null
private var mCalculateWholeLines = true
private val mLines: MutableList<Line> = ArrayList()
private var mDropLastIfSpace = true
private var mMoreText: String? = null
private var mMoreTextColor = 0
private var mMoreTextTypeface: Typeface? = null
private var mMoreUnderlineColor = Color.TRANSPARENT
private var mMoreBgColor = 0
private var mMoreUnderlineHeight = 0
private var mTotalLineCount = 0
class LineLayout {
var maxLines = Int.MAX_VALUE
var ellipsize: TruncateAt? = null
var calculateWholeLines = false
var dropLastIfSpace = true
var moreText: String? = null
var moreTextColor = 0
var moreTextTypeface: Typeface? = null
var moreUnderlineColor = Color.TRANSPARENT
var moreBgColor = 0
var moreUnderlineHeight = 0
var typeModel: TypeModel? = null
private val mLines: MutableList<Line> = ArrayList()
var totalLineCount = 0
private set
fun setMaxLines(maxLines: Int): LineLayout {
mMaxLines = maxLines
return this
}
fun setEllipsize(ellipsize: TruncateAt?): LineLayout {
mEllipsize = ellipsize
return this
}
fun setCalculateWholeLines(calculateWholeLines: Boolean): LineLayout {
mCalculateWholeLines = calculateWholeLines
return this
}
fun setDropLastIfSpace(dropLastIfSpace: Boolean): LineLayout {
mDropLastIfSpace = dropLastIfSpace
return this
}
fun setMoreText(text: String?, color: Int, typeface: Typeface?): LineLayout {
mMoreText = text
mMoreTextColor = color
mMoreTextTypeface = typeface
return this
}
fun setMoreBackgroundColor(color: Int): LineLayout {
mMoreBgColor = color
return this
}
fun setUnderline(height: Int, color: Int): LineLayout {
mMoreUnderlineHeight = height
mMoreUnderlineColor = color
return this
}
fun setTypeModel(typeModel: TypeModel?): LineLayout {
this.typeModel = typeModel
return this
}
fun measureAndLayout() {
mTypeEnvironment.clear()
fun measureAndLayout(env: TypeEnvironment) {
env.clear()
release()
if (typeModel == null) {
return
}
var element: Element? = typeModel!!.firstElement() ?: return
var element: Element? = typeModel!!.firstElement()
var line = Line.acquire()
var y = 0
line.init(0, y, mTypeEnvironment.widthLimit)
line.init(0, y, env.widthLimit)
while (element != null) {
element.measure(mTypeEnvironment)
element.measure(env)
if (element is NextParagraphElement) {
line.add(element)
line.layout(mTypeEnvironment, mDropLastIfSpace, false)
line.layout(env, dropLastIfSpace, false)
mLines.add(line)
if (canInterrupt()) {
return
}
y += line.contentHeight + mTypeEnvironment.paragraphSpace
line = createNewLine(y)
} else if (line.contentWidth + element.measureWidth > mTypeEnvironment.widthLimit) {
y += line.contentHeight + env.paragraphSpace
line = createNewLine(env, y)
} else if (line.contentWidth + element.measureWidth > env.widthLimit) {
if (mLines.size == 0 && line.size == 0) {
// the width is too small.
line.release()
return
}
val back = line.handleWordBreak(mTypeEnvironment)
line.layout(mTypeEnvironment, mDropLastIfSpace, false)
val back = line.handleWordBreak(env)
line.layout(env, dropLastIfSpace, false)
mLines.add(line)
if (canInterrupt()) {
handleEllipse(true)
handleEllipse(env,true)
return
}
y += line.contentHeight + mTypeEnvironment.lineSpace
line = createNewLine(y)
y += line.contentHeight + env.lineSpace
line = createNewLine(env, y)
if (back != null && !back.isEmpty()) {
for (el in back) {
line.add(el)
@@ -130,37 +92,39 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
element = element.next
}
if (line.size > 0) {
line.layout(mTypeEnvironment, mDropLastIfSpace, true)
line.layout(env, dropLastIfSpace, true)
mLines.add(line)
} else {
line.release()
}
mTotalLineCount = mLines.size
handleEllipse(false)
totalLineCount = mLines.size
handleEllipse(env,false)
}
private fun createNewLine(y: Int): Line {
private fun createNewLine(env: TypeEnvironment, y: Int): Line {
val line = Line.acquire()
line.init(0, y, mTypeEnvironment.widthLimit)
line.init(0, y, env.widthLimit)
return line
}
private fun handleEllipse(fromInterrupt: Boolean) {
if (mLines.isEmpty() || mLines.size < mMaxLines || mLines.size == mMaxLines && !fromInterrupt) {
private fun handleEllipse(env: TypeEnvironment, fromInterrupt: Boolean) {
if (mLines.isEmpty() || mLines.size < maxLines || mLines.size == maxLines && !fromInterrupt) {
return
}
if (mEllipsize == TruncateAt.END) {
handleEllipseEnd()
} else if (mEllipsize == TruncateAt.START) {
handleEllipseStart()
} else if (mEllipsize == TruncateAt.MIDDLE) {
handleEllipseMiddle()
if (ellipsize == TruncateAt.END) {
handleEllipseEnd(env)
} else if (ellipsize == TruncateAt.START) {
handleEllipseStart(env)
} else if (ellipsize == TruncateAt.MIDDLE) {
handleEllipseMiddle(env)
}
}
private fun handleEllipseEnd() {
for (i in mLines.size - 1 downTo mMaxLines) {
mLines.remove(mLines[i])
private fun handleEllipseEnd(env: TypeEnvironment) {
for (i in mLines.size - 1 downTo maxLines) {
val line = mLines[i]
mLines.remove(line)
line.release()
}
val lastLine = mLines[mLines.size - 1]
var limitWidth = lastLine.widthLimit
@@ -170,11 +134,11 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
env.clear()
}
})
ellipseElement.measure(mTypeEnvironment)
ellipseElement.measure(env)
limitWidth = (limitWidth - ellipseElement.measureWidth).toInt()
var moreElement: Element? = null
if (mMoreText != null && !mMoreText!!.isEmpty()) {
moreElement = TextElement(mMoreText!!, -1, -1)
if (moreText != null && !moreText!!.isEmpty()) {
moreElement = TextElement(moreText!!, -1, -1)
val changeTypes: MutableList<Int> = ArrayList()
changeTypes.add(TypeEnvironment.TYPE_TEXT_COLOR)
changeTypes.add(TypeEnvironment.TYPE_BG_COLOR)
@@ -183,21 +147,21 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
changeTypes.add(TypeEnvironment.TYPE_BORDER_BOTTOM_WIDTH)
moreElement.addSingleEnvironmentUpdater(changeTypes, object : EnvironmentUpdater {
override fun update(env: TypeEnvironment) {
if (mMoreTextColor != 0) {
env.textColor = mMoreTextColor
if (moreTextColor != 0) {
env.textColor = moreTextColor
}
if (mMoreBgColor != 0) {
env.backgroundColor = mMoreBgColor
if (moreBgColor != 0) {
env.backgroundColor = moreBgColor
}
if (mMoreTextTypeface != null) {
env.typeface = mMoreTextTypeface
if (moreTextTypeface != null) {
env.typeface = moreTextTypeface
}
if (mMoreUnderlineHeight > 0) {
env.setBorderBottom(mMoreUnderlineHeight, mMoreUnderlineColor)
if (moreUnderlineHeight > 0) {
env.setBorderBottom(moreUnderlineHeight, moreUnderlineColor)
}
}
})
moreElement.measure(mTypeEnvironment)
moreElement.measure(env)
limitWidth = (limitWidth - moreElement.measureWidth).toInt()
}
val contentWidth = lastLine.contentWidth
@@ -208,7 +172,7 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
for (el in elements) {
if (el.measureWidth <= limitWidth) {
lastLine.add(el)
limitWidth -= (limitWidth - el.measureWidth).toInt()
limitWidth = (limitWidth - el.measureWidth).toInt()
} else {
break
}
@@ -218,12 +182,12 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
if (moreElement != null) {
lastLine.add(moreElement)
}
lastLine.layout(mTypeEnvironment, mDropLastIfSpace, true)
lastLine.layout(env, dropLastIfSpace, true)
}
private fun handleEllipseStart() {
mTypeEnvironment.clear()
for (i in mLines.size - 1 downTo mMaxLines) {
private fun handleEllipseStart(env: TypeEnvironment) {
env.clear()
for (i in mLines.size - 1 downTo maxLines) {
mLines.remove(mLines[i])
}
val ellipseElement: Element = TextElement("...", -1, -1)
@@ -232,7 +196,7 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
env.clear()
}
})
ellipseElement.measure(mTypeEnvironment)
ellipseElement.measure(env)
val elements: Queue<Element> = LinkedList()
elements.add(ellipseElement)
for (i in mLines.indices) {
@@ -245,7 +209,7 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
if (el is NextParagraphElement) {
elements.poll()
line.add(el)
el.move(mTypeEnvironment)
el.move(env)
break
}
if (el is BreakWordLineElement) {
@@ -255,7 +219,7 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
if (line.contentWidth + el.measureWidth <= limitWidth) {
elements.poll()
line.add(el)
el.move(mTypeEnvironment)
el.move(env)
} else {
break
}
@@ -263,21 +227,21 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
elements.poll()
}
}
line.handleWordBreak(mTypeEnvironment)
line.layout(mTypeEnvironment, mDropLastIfSpace, false)
line.handleWordBreak(env)
line.layout(env, dropLastIfSpace, false)
if (elements.isEmpty()) {
return
}
}
}
private fun handleEllipseMiddle() {
mTypeEnvironment.clear()
private fun handleEllipseMiddle(env: TypeEnvironment) {
env.clear()
val lines: List<Line> = ArrayList(mLines)
mLines.clear()
val ellipseElement: Element = TextElement("...", -1, -1)
ellipseElement.measure(mTypeEnvironment)
val ellipseLine = if (mMaxLines % 2 == 0) mMaxLines / 2 else (mMaxLines + 1) / 2
ellipseElement.measure(env)
val ellipseLine = if (maxLines % 2 == 0) maxLines / 2 else (maxLines + 1) / 2
for (i in 0 until ellipseLine) {
mLines.add(lines[i])
}
@@ -290,7 +254,7 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
if (handleLine.contentWidth + el.measureWidth <= limitWidth / 2f - ellipseElement.measureWidth / 2) {
unHandled.poll()
handleLine.add(el)
el.move(mTypeEnvironment)
el.move(env)
} else {
break
}
@@ -298,9 +262,9 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
unHandled.poll()
}
}
ellipseElement.measure(mTypeEnvironment)
ellipseElement.measure(env)
handleLine.add(ellipseElement)
val nextFullShowLine = lines.size - mMaxLines + ellipseLine
val nextFullShowLine = lines.size - maxLines + ellipseLine
var startLine = lines.size - 1
// find the latest paragraph end line.
for (i in lines.size - 2 downTo nextFullShowLine + 1) {
@@ -372,29 +336,29 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
}
if (ellipseEffect.size > 0) {
val ignoreEffectElement = IgnoreEffectElement(ellipseEffect)
ignoreEffectElement.move(mTypeEnvironment)
ignoreEffectElement.move(env)
handleLine.add(ignoreEffectElement)
}
}
for (el in toAdd) {
el.move(mTypeEnvironment)
el.move(env)
handleLine.add(el)
}
handleLine.handleWordBreak(mTypeEnvironment)
handleLine.layout(mTypeEnvironment, mDropLastIfSpace, ellipseLine == lines.size)
handleLine.handleWordBreak(env)
handleLine.layout(env, dropLastIfSpace, ellipseLine == lines.size)
var lastEnd = handleLine.y + handleLine.contentHeight
for (i in nextFullShowLine until lines.size) {
val line = lines[i]
val prev = lines[i - 1]
if (prev.isMiddleParagraphEndLine) {
line.y = lastEnd + mTypeEnvironment.paragraphSpace
line.y = lastEnd + env.paragraphSpace
} else {
line.y = lastEnd + mTypeEnvironment.lineSpace
line.y = lastEnd + env.lineSpace
}
lastEnd = line.y + line.contentHeight
line.move(mTypeEnvironment)
line.handleWordBreak(mTypeEnvironment)
line.layout(mTypeEnvironment, mDropLastIfSpace, i == lines.size - 1)
line.move(env)
line.handleWordBreak(env)
line.layout(env, dropLastIfSpace, i == lines.size - 1)
mLines.add(line)
}
}
@@ -416,16 +380,16 @@ class LineLayout(private val mTypeEnvironment: TypeEnvironment) {
return last.y + last.contentHeight
}
fun draw(canvas: Canvas) {
mTypeEnvironment.clear()
fun draw(canvas: Canvas, env: TypeEnvironment) {
env.clear()
for (line in mLines) {
line.draw(mTypeEnvironment, canvas)
line.draw(env, canvas)
}
}
private fun canInterrupt(): Boolean {
return mLines.size == mMaxLines && !mCalculateWholeLines &&
(mEllipsize == null || mEllipsize == TruncateAt.END)
return mLines.size == maxLines && !calculateWholeLines &&
(ellipsize == null || ellipsize == TruncateAt.END)
}
fun release() {
@@ -62,6 +62,7 @@ class TypeEnvironment {
val paint = Paint().apply {
isAntiAlias = true
textSize = Resources.getSystem().displayMetrics.scaledDensity * 14f
}
val bgPaint = Paint().apply {
isAntiAlias = true
@@ -81,7 +82,7 @@ class TypeEnvironment {
paint.typeface = value
}
var textSize: Float = 0f
var textSize: Float = paint.textSize
set(value) {
field = value
paint.textSize = value
@@ -98,7 +99,7 @@ class TypeEnvironment {
}
fun setCustomProp(type: Int, value: Any?) {
mCustomProp!!.put(type, value)
mCustomProp.put(type, value)
}
fun setBorderTop(width: Int, color: Int) {
@@ -257,4 +258,14 @@ class TypeEnvironment {
}
}
}
fun isRunning(): Boolean{
for (i in 0 until mStack.size()) {
val stack = mStack.valueAt(i)
if(stack.size > 0){
return true
}
}
return false
}
}
@@ -15,6 +15,7 @@
*/
package com.qmuiteam.qmui.type
import android.graphics.Typeface
import com.qmuiteam.qmui.type.element.Element
import java.util.*
@@ -26,6 +27,26 @@ class TypeModel(
var firstEffect: Element? = null
fun addTypefaceEffect(start: Int, end: Int, typeface: Typeface): EffectRemover? {
val types: MutableList<Int> = ArrayList()
types.add(TypeEnvironment.TYPE_TYPEFACE)
return unsafeAddEffect(start, end, types, object : EnvironmentUpdater {
override fun update(env: TypeEnvironment) {
env.typeface = typeface
}
})
}
fun addTextSizeEffect(start: Int, end: Int, textSize: Float): EffectRemover? {
val types: MutableList<Int> = ArrayList()
types.add(TypeEnvironment.TYPE_TEXT_SIZE)
return unsafeAddEffect(start, end, types, object : EnvironmentUpdater {
override fun update(env: TypeEnvironment) {
env.textSize = textSize
}
})
}
fun addBgEffect(start: Int, end: Int, bgColor: Int): EffectRemover? {
val types: MutableList<Int> = ArrayList()
types.add(TypeEnvironment.TYPE_BG_COLOR)
@@ -260,9 +260,9 @@ abstract class Element(val text: CharSequence, val index: Int, val start: Int) {
saveTypeList?.forEach {
env.save(it)
}
// environmentUpdaterList?.forEach {
// it.update(env)
// }
environmentUpdaterList?.forEach {
it.update(env)
}
}
fun restoreEnv(env: TypeEnvironment) {
@@ -23,6 +23,13 @@ import com.qmuiteam.qmui.type.parser.ParserHelper.handleWordPart
import java.util.*
class PlainTextParser : TextParser {
companion object {
val instance by lazy {
PlainTextParser()
}
}
override fun parse(text: CharSequence?): TypeModel? {
if (text == null || text.isEmpty()) {
return null
@@ -0,0 +1,86 @@
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.type.view
import android.content.Context
import android.graphics.Typeface
import android.util.AttributeSet
import android.view.View
import com.qmuiteam.qmui.type.TypeEnvironment
open class BaseTypeView(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0): View(context, attrs, defStyleAttr) {
val environment = TypeEnvironment()
var textSize: Float
get() = environment.textSize
set(value){
throwIfRunning("setTextSize")
if(environment.textSize != value){
environment.textSize = value
requestLayout()
}
}
var textColor: Int
get() = environment.textColor
set(value){
throwIfRunning("setTextColor")
if(environment.textColor != value){
environment.textColor = value
invalidate()
}
}
var typeface: Typeface?
get() = environment.typeface
set(value){
throwIfRunning("setTypeface")
if(environment.typeface != value){
environment.typeface = value
requestLayout()
}
}
var lineSpace: Int
get() = environment.lineSpace
set(value){
throwIfRunning("setLineSpace")
if(environment.lineSpace != value){
environment.lineSpace = value
requestLayout()
}
}
var paragraphSpace: Int
get() = environment.paragraphSpace
set(value){
throwIfRunning("setParagraphSpace")
if(environment.paragraphSpace != value){
environment.paragraphSpace = value
requestLayout()
}
}
fun throwIfRunning(action: String) {
if(environment.isRunning()){
throw RuntimeException("can not perform $action when running.")
}
}
}
@@ -0,0 +1,121 @@
/*
* Tencent is pleased to support the open source community by making QMUI_Android available.
*
* Copyright (C) 2017-2018 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.qmuiteam.qmui.type.view
import android.content.Context
import android.graphics.Canvas
import android.graphics.Typeface
import android.text.TextUtils
import android.util.AttributeSet
import androidx.annotation.ColorInt
import com.qmuiteam.qmui.type.LineLayout
import com.qmuiteam.qmui.type.TypeModel
import com.qmuiteam.qmui.type.parser.PlainTextParser
import com.qmuiteam.qmui.type.parser.TextParser
open class LineTypeView : BaseTypeView {
val lineLayout = LineLayout()
var textParser: TextParser = PlainTextParser.instance
set(value) {
if (field != value) {
field = value
lineLayout.typeModel = value.parse(text)
requestLayout()
}
}
constructor(context: Context): super(context)
constructor(context: Context, attrs: AttributeSet?): super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
environment.setMeasureLimit(widthSize, heightSize)
lineLayout.measureAndLayout(environment)
val usedWidth = if (widthMode == MeasureSpec.AT_MOST) {
lineLayout.maxLayoutWidth
} else widthSize
val usedHeight = if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
lineLayout.contentHeight
} else heightSize
setMeasuredDimension(usedWidth, usedHeight)
}
var text: CharSequence? = null
set(value) {
if (field != value) {
field = value
lineLayout.typeModel = textParser.parse(value)
requestLayout()
}
}
var ellipsized: TextUtils.TruncateAt?
get() = lineLayout.ellipsize
set(value) {
if (lineLayout.ellipsize != value) {
lineLayout.ellipsize = value
requestLayout()
}
}
fun addBgEffect(start: Int, end: Int, @ColorInt color: Int): TypeModel.EffectRemover? {
val typeModel = lineLayout.typeModel ?: return null
val remover = typeModel.addBgEffect(start, end, color) ?: return null
invalidate()
return remover
}
fun addTextColorEffect(start: Int, end: Int, @ColorInt color: Int): TypeModel.EffectRemover? {
val typeModel = lineLayout.typeModel ?: return null
val remover = typeModel.addTextColorEffect(start, end, color) ?: return null
invalidate()
return remover
}
fun addUnderLineEffect(start: Int, end: Int, @ColorInt color: Int, height: Int): TypeModel.EffectRemover? {
val typeModel = lineLayout.typeModel ?: return null
val remover = typeModel.addUnderLineEffect(start, end, color, height) ?: return null
invalidate()
return remover
}
fun addTypefaceEffect(start: Int, end: Int, typeface: Typeface): TypeModel.EffectRemover? {
val typeModel = lineLayout.typeModel ?: return null
val remover = typeModel.addTypefaceEffect(start, end, typeface) ?: return null
requestLayout()
return remover
}
fun addTextSizeEffect(start: Int, end: Int, textSize: Float): TypeModel.EffectRemover? {
val typeModel = lineLayout.typeModel ?: return null
val remover = typeModel.addTextSizeEffect(start, end, textSize) ?: return null
requestLayout()
return remover
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
lineLayout.draw(canvas, environment)
}
}
@@ -0,0 +1,228 @@
package com.qmuiteam.qmui.type.view
import android.content.Context
import android.graphics.*
import android.os.Build
import android.os.SystemClock
import android.util.AttributeSet
import androidx.annotation.RequiresApi
import com.qmuiteam.qmui.type.TypeModel
import com.qmuiteam.qmui.type.parser.PlainTextParser
import com.qmuiteam.qmui.type.parser.TextParser
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class MarqueeTypeView : BaseTypeView {
constructor(context: Context): super(context)
constructor(context: Context, attrs: AttributeSet?): super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr)
var typeModel: TypeModel? = null
var textParser: TextParser = PlainTextParser.instance
set(value) {
if (field != value) {
field = value
typeModel = value.parse(text)
requestLayout()
if (isAttachedToWindow) {
start()
}
}
}
var text: CharSequence? = null
set(value) {
if (field != value) {
field = value
reset()
typeModel = textParser.parse(value)
requestLayout()
if (isAttachedToWindow) {
start()
}
}
}
var gap: Float = resources.displayMetrics.density * 50
var moveSpeedPerMs: Float = resources.displayMetrics.density / 32
var lastDrawTime = -2L
private var elementMaxHeight: Float = 0f
private var contentWidth: Float = 0f
private var fadeHelper: FadeHelper? = null
private var startX: Float = 0f
var fadeWidth: Float
get() = fadeHelper?.fadeWidth ?: 0f
set(value) {
(fadeHelper ?: FadeHelper().also { fadeHelper = it }).fadeWidth = value
invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
environment.setMeasureLimit(widthSize, heightSize)
measureAndLayoutModel()
val usedWidth = if (widthMode == MeasureSpec.AT_MOST) {
contentWidth.toInt()
} else widthSize
val usedHeight = if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
elementMaxHeight.toInt()
} else heightSize
var current = typeModel?.firstElement()
while (current != null) {
current.y = (usedHeight - current.measureHeight) / 2
current = current.next
}
setMeasuredDimension(usedWidth, usedHeight)
}
private fun measureAndLayoutModel() {
elementMaxHeight = 0f
var current = typeModel?.firstElement()
var x = 0f
while (current != null) {
current.measure(environment)
current.x = x
elementMaxHeight = elementMaxHeight.coerceAtLeast(current.measureHeight)
x += current.measureWidth
current = current.next
}
contentWidth = x
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (!text.isNullOrBlank()) {
start()
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
stop()
}
fun start() {
lastDrawTime = -1
}
fun stop() {
lastDrawTime = -2
}
fun reset() {
startX = 0f
}
override fun onDraw(canvas: Canvas) {
if(fadeHelper == null){
drawContent(canvas)
}else{
fadeHelper!!.drawFade(canvas){
drawContent(canvas)
}
}
}
private fun drawContent(canvas: Canvas){
if (contentWidth <= width) {
lastDrawTime = -2
startX = 0f
}
canvas.save()
canvas.translate(startX, 0f)
var current = typeModel?.firstElement()
while (current != null) {
if (current.x + startX > width) {
break
} else if (current.x + startX + current.measureWidth > 0) {
current.draw(environment, canvas)
}
current = current.next
}
canvas.restore()
val gapRight = startX + contentWidth + gap
if (startX < 0 && gapRight < width) {
canvas.save()
canvas.translate(gapRight, 0f)
current = typeModel?.firstElement()
while (current != null) {
if (current.x + gapRight > width) {
break
} else {
current.draw(environment, canvas)
}
current = current.next
}
canvas.restore()
}
if (lastDrawTime == -2L) {
return
}
if (lastDrawTime == -1L) {
lastDrawTime = SystemClock.elapsedRealtime()
} else {
val newTime = SystemClock.elapsedRealtime()
startX -= moveSpeedPerMs * (newTime - lastDrawTime)
lastDrawTime = newTime
if (startX + contentWidth < 0) {
startX += contentWidth + gap
}
// if drop many many frames. this condition can be matched, so recover to normal state.
if (startX + contentWidth < 0) {
startX = 0f
}
}
postInvalidateOnAnimation()
}
private inner class FadeHelper{
var fadeWidth = 0f
private val paint = Paint().apply {
style = Paint.Style.FILL
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
}
private val leftFadeShader by lazy(LazyThreadSafetyMode.NONE) {
LinearGradient(
0f, 0f, fadeWidth, 0f,
intArrayOf(Color.TRANSPARENT, Color.BLACK), null, Shader.TileMode.CLAMP
)
}
private val rightFadeShader by lazy(LazyThreadSafetyMode.NONE) {
LinearGradient(
0f, 0f, fadeWidth, 0f,
intArrayOf(Color.BLACK, Color.TRANSPARENT), null, Shader.TileMode.CLAMP
)
}
inline fun drawFade(canvas: Canvas, action: (Canvas) -> Unit) = canvas.apply {
if(fadeWidth <= 0 || contentWidth < width){
action(this)
}else{
val layerId = saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null)
action(this)
if(startX < 0){
paint.shader = leftFadeShader
drawRect(0f, 0f, fadeWidth, height.toFloat(), paint)
}
translate((width - fadeWidth).coerceAtLeast(0f), 0f)
paint.shader = rightFadeShader
drawRect(0f, 0f, fadeWidth, height.toFloat(), paint)
restoreToCount(layerId)
}
}
}
}