mirror of
https://github.com/Tencent/QMUI_Android.git
synced 2026-10-17 04:31:16 +08:00
feature: MarqueeTypeView
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user