This commit is contained in:
Benoit Marty 2023-06-23 18:15:22 +02:00
parent e9b9434671
commit 69680a9856
12 changed files with 487 additions and 7 deletions

View File

@ -240,7 +240,6 @@ ext.groups = [
], ],
group: [ group: [
'me.dm7.barcodescanner', 'me.dm7.barcodescanner',
'me.gujun.android',
] ]
] ]
] ]

View File

@ -58,9 +58,7 @@ dependencies {
implementation libs.airbnb.mavericks implementation libs.airbnb.mavericks
// Span utils // Span utils
implementation('me.gujun.android:span:1.7') { implementation project(":library:external:span")
exclude group: 'com.android.support', module: 'support-annotations'
}
implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesCore
implementation libs.jetbrains.coroutinesAndroid implementation libs.jetbrains.coroutinesAndroid

20
library/external/span/build.gradle vendored Normal file
View File

@ -0,0 +1,20 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
namespace "me.gujun.android.span"
compileSdk versions.compileSdk
defaultConfig {
minSdk versions.minSdk
targetSdk versions.targetSdk
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
}
dependencies {
implementation 'com.android.support:support-annotations:28.0.0'
}

View File

@ -0,0 +1,316 @@
package me.gujun.android.span
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Layout
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.text.style.AbsoluteSizeSpan
import android.text.style.AlignmentSpan
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
import android.text.style.ImageSpan
import android.text.style.QuoteSpan
import android.text.style.StyleSpan
import android.text.style.SubscriptSpan
import android.text.style.SuperscriptSpan
import android.text.style.TypefaceSpan
import android.text.style.URLSpan
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.Dimension
import me.gujun.android.span.style.CustomTypefaceSpan
import me.gujun.android.span.style.LineSpacingSpan
import me.gujun.android.span.style.SimpleClickableSpan
import me.gujun.android.span.style.TextDecorationLineSpan
import me.gujun.android.span.style.VerticalPaddingSpan
class Span(val parent: Span? = null) : SpannableStringBuilder() {
companion object {
val EMPTY_STYLE = Span()
var globalStyle: Span = EMPTY_STYLE
}
var text: CharSequence = ""
@ColorInt var textColor: Int? = parent?.textColor
@ColorInt var backgroundColor: Int? = parent?.backgroundColor
@Dimension(unit = Dimension.PX) var textSize: Int? = parent?.textSize
var fontFamily: String? = parent?.fontFamily
var typeface: Typeface? = parent?.typeface
var textStyle: String? = parent?.textStyle
var alignment: String? = parent?.alignment
var textDecorationLine: String? = parent?.textDecorationLine
@Dimension(unit = Dimension.PX) var lineSpacing: Int? = null
@Dimension(unit = Dimension.PX) var paddingTop: Int? = null
@Dimension(unit = Dimension.PX) var paddingBottom: Int? = null
@Dimension(unit = Dimension.PX) var verticalPadding: Int? = null
var onClick: (() -> Unit)? = null
var spans: ArrayList<Any> = ArrayList()
var style: Span = EMPTY_STYLE
private fun buildCharacterStyle(builder: ArrayList<Any>) {
if (textColor != null) {
builder.add(ForegroundColorSpan(textColor!!))
}
if (backgroundColor != null) {
builder.add(BackgroundColorSpan(backgroundColor!!))
}
if (textSize != null) {
builder.add(AbsoluteSizeSpan(textSize!!))
}
if (!TextUtils.isEmpty(fontFamily)) {
builder.add(TypefaceSpan(fontFamily))
}
if (typeface != null) {
builder.add(CustomTypefaceSpan(typeface!!))
}
if (!TextUtils.isEmpty(textStyle)) {
builder.add(StyleSpan(when (textStyle) {
"normal" -> Typeface.NORMAL
"bold" -> Typeface.BOLD
"italic" -> Typeface.ITALIC
"bold_italic" -> Typeface.BOLD_ITALIC
else -> throw RuntimeException("Unknown text style")
}))
}
if (!TextUtils.isEmpty(textDecorationLine)) {
builder.add(TextDecorationLineSpan(textDecorationLine!!))
}
if (onClick != null) {
builder.add(object : SimpleClickableSpan() {
override fun onClick(widget: View) {
onClick?.invoke()
}
})
}
}
private fun buildParagraphStyle(builder: ArrayList<Any>) {
if (!TextUtils.isEmpty(alignment)) {
builder.add(AlignmentSpan.Standard(when (alignment) {
"normal" -> Layout.Alignment.ALIGN_NORMAL
"opposite" -> Layout.Alignment.ALIGN_OPPOSITE
"center" -> Layout.Alignment.ALIGN_CENTER
else -> throw RuntimeException("Unknown text alignment")
}))
}
if (lineSpacing != null) {
builder.add(LineSpacingSpan(lineSpacing!!))
}
paddingTop = when {
paddingTop != null -> paddingTop
verticalPadding != null -> verticalPadding
else -> 0
}
paddingBottom = when {
paddingBottom != null -> paddingBottom
verticalPadding != null -> verticalPadding
else -> 0
}
if (paddingTop != 0 || paddingBottom != 0) {
builder.add(VerticalPaddingSpan(paddingTop!!, paddingBottom!!))
}
}
private fun prebuild() {
override(style)
}
fun build(): Span {
prebuild()
val builder = ArrayList<Any>()
if (!TextUtils.isEmpty(text)) {
var p = this.parent
while (p != null) {
if (!TextUtils.isEmpty(p.text)) {
throw RuntimeException("Can't nest \"$text\" in spans")
}
p = p.parent
}
append(text)
buildCharacterStyle(builder)
buildParagraphStyle(builder)
} else {
buildParagraphStyle(builder)
}
builder.addAll(spans)
builder.forEach {
setSpan(it, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
return this
}
fun override(style: Span) {
if (textColor == null) {
textColor = style.textColor
}
if (backgroundColor == null) {
backgroundColor = style.backgroundColor
}
if (textSize == null) {
textSize = style.textSize
}
if (fontFamily == null) {
fontFamily = style.fontFamily
}
if (typeface == null) {
typeface = style.typeface
}
if (textStyle == null) {
textStyle = style.textStyle
}
if (alignment == null) {
alignment = style.alignment
}
if (textDecorationLine == null) {
textDecorationLine = style.textDecorationLine
}
if (lineSpacing == null) {
lineSpacing = style.lineSpacing
}
if (paddingTop == null) {
paddingTop = style.paddingTop
}
if (paddingBottom == null) {
paddingBottom = style.paddingBottom
}
if (verticalPadding == null) {
verticalPadding = style.verticalPadding
}
if (onClick == null) {
onClick = style.onClick
}
spans.addAll(style.spans)
}
operator fun CharSequence.unaryPlus(): CharSequence {
return append(Span(parent = this@Span).apply {
text = this@unaryPlus
build()
})
}
operator fun Span.plus(other: CharSequence): CharSequence {
return append(Span(parent = this).apply {
text = other
build()
})
}
}
fun span(init: Span.() -> Unit): Span = Span().apply {
override(Span.globalStyle)
init()
build()
}
fun span(text: CharSequence, init: Span.() -> Unit): Span = Span().apply {
override(Span.globalStyle)
this.text = text
init()
build()
}
fun style(init: Span.() -> Unit): Span = Span().apply {
init()
}
fun Span.span(init: Span.() -> Unit = {}): Span = apply {
append(Span(parent = this).apply {
init()
build()
})
}
fun Span.span(text: CharSequence, init: Span.() -> Unit = {}): Span = apply {
append(Span(parent = this).apply {
this.text = text
init()
build()
})
}
fun Span.link(url: String, text: CharSequence = "",
init: Span.() -> Unit = {}): Span = apply {
append(Span(parent = this).apply {
this.text = text
this.spans.add(URLSpan(url))
init()
build()
})
}
fun Span.quote(@ColorInt color: Int, text: CharSequence = "",
init: Span.() -> Unit = {}): Span = apply {
append(Span(parent = this).apply {
this.text = text
this.spans.add(QuoteSpan(color))
init()
build()
})
}
fun Span.superscript(text: CharSequence = "", init: Span.() -> Unit = {}): Span = apply {
append(Span(parent = this).apply {
this.text = text
this.spans.add(SuperscriptSpan())
init()
build()
})
}
fun Span.subscript(text: CharSequence = "", init: Span.() -> Unit = {}): Span = apply {
append(Span(parent = this).apply {
this.text = text
this.spans.add(SubscriptSpan())
init()
build()
})
}
fun Span.image(drawable: Drawable, alignment: String = "bottom",
init: Span.() -> Unit = {}): Span = apply {
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
append(Span(parent = this).apply {
this.text = " "
this.spans.add(ImageSpan(drawable, when (alignment) {
"bottom" -> ImageSpan.ALIGN_BOTTOM
"baseline" -> ImageSpan.ALIGN_BASELINE
else -> throw RuntimeException("Unknown image alignment")
}))
init()
build()
})
}
fun Span.addSpan(what: Any) = apply {
this.spans.add(what)
}

View File

@ -0,0 +1,36 @@
package me.gujun.android.span.style
import android.graphics.Paint
import android.graphics.Typeface
import android.text.TextPaint
import android.text.style.MetricAffectingSpan
class CustomTypefaceSpan(private val tf: Typeface) : MetricAffectingSpan() {
override fun updateMeasureState(paint: TextPaint) {
apply(paint, tf)
}
override fun updateDrawState(ds: TextPaint) {
apply(ds, tf)
}
private fun apply(paint: Paint, tf: Typeface) {
val oldStyle: Int
val old = paint.typeface
oldStyle = old?.style ?: 0
val fake = oldStyle and tf.style.inv()
if (fake and Typeface.BOLD != 0) {
paint.isFakeBoldText = true
}
if (fake and Typeface.ITALIC != 0) {
paint.textSkewX = -0.25f
}
paint.typeface = tf
}
}

View File

@ -0,0 +1,31 @@
package me.gujun.android.span.style
import android.graphics.Paint.FontMetricsInt
import android.text.Spanned
import android.text.style.LineHeightSpan
class LineSpacingSpan(private val add: Int) : LineHeightSpan {
override fun chooseHeight(text: CharSequence, start: Int, end: Int, spanstartv: Int, v: Int,
fm: FontMetricsInt) {
text as Spanned
/*val spanStart =*/ text.getSpanStart(this)
val spanEnd = text.getSpanEnd(this)
// Log.d("DEBUG", "Text: start=$start end=$end v=$v") // end may include the \n character
// Log.d("DEBUG", "${text.slice(start until end)}".replace("\n", "#"))
// Log.d("DEBUG", "LineSpacingSpan: spanStart=$spanStart spanEnd=$spanEnd spanstartv=$spanstartv")
// Log.d("DEBUG", "$fm")
// Log.d("DEBUG", "-----------------------")
if (spanstartv == v) {
fm.descent += add
} else if (text[start - 1] == '\n') {
fm.descent += add
}
if (end == spanEnd || end - 1 == spanEnd) {
fm.descent -= add
}
}
}

View File

@ -0,0 +1,10 @@
package me.gujun.android.span.style
import android.text.TextPaint
import android.text.style.ClickableSpan
abstract class SimpleClickableSpan : ClickableSpan() {
override fun updateDrawState(ds: TextPaint) {
// no-op
}
}

View File

@ -0,0 +1,29 @@
package me.gujun.android.span.style
import android.text.TextPaint
import android.text.style.CharacterStyle
class TextDecorationLineSpan(private val textDecorationLine: String) : CharacterStyle() {
override fun updateDrawState(tp: TextPaint) {
when (textDecorationLine) {
"none" -> {
tp.isUnderlineText = false
tp.isStrikeThruText = false
}
"underline" -> {
tp.isUnderlineText = true
tp.isStrikeThruText = false
}
"line-through" -> {
tp.isUnderlineText = false
tp.isStrikeThruText = true
}
"underline line-through" -> {
tp.isUnderlineText = true
tp.isStrikeThruText = true
}
else -> throw RuntimeException("Unknown text decoration line")
}
}
}

View File

@ -0,0 +1,41 @@
package me.gujun.android.span.style
import android.graphics.Paint.FontMetricsInt
import android.text.Spanned
import android.text.style.LineHeightSpan
class VerticalPaddingSpan(private val paddingTop: Int,
private val paddingBottom: Int) : LineHeightSpan {
private var flag: Boolean = true
override fun chooseHeight(text: CharSequence, start: Int, end: Int, spanstartv: Int, v: Int,
fm: FontMetricsInt) {
text as Spanned
/*val spanStart =*/ text.getSpanStart(this)
val spanEnd = text.getSpanEnd(this)
// Log.d("DEBUG", "Text: start=$start end=$end v=$v") // end may include the \n character
// Log.d("DEBUG", "${text.slice(start until end)}".replace("\n", "#"))
// Log.d("DEBUG", "VerticalPadding: spanStart=$spanStart spanEnd=$spanEnd spanstartv=$spanstartv")
// Log.d("DEBUG", "$fm")
// Log.d("DEBUG", "-----------------------")
if (spanstartv == v) {
fm.top -= paddingTop
fm.ascent -= paddingTop
flag = true
} else if (flag && text[start - 1] != '\n') {
fm.top += paddingTop
fm.ascent += paddingTop
flag = false
} else {
flag = false
}
if (end == spanEnd || end - 1 == spanEnd) {
fm.descent += paddingBottom
fm.bottom += paddingBottom
}
}
}

View File

@ -14,6 +14,7 @@ include ':library:external:dialpad'
include ':library:external:textdrawable' include ':library:external:textdrawable'
include ':library:external:autocomplete' include ':library:external:autocomplete'
include ':library:external:realmfieldnameshelper' include ':library:external:realmfieldnameshelper'
include ':library:external:span'
include ':library:rustCrypto' include ':library:rustCrypto'
include ':matrix-sdk-android' include ':matrix-sdk-android'

View File

@ -396,6 +396,7 @@ dependencies {
implementation project(':vector') implementation project(':vector')
implementation project(':vector-config') implementation project(':vector-config')
implementation project(':library:core-utils') implementation project(':library:core-utils')
debugImplementation project(':library:external:span')
debugImplementation project(':library:ui-styles') debugImplementation project(':library:ui-styles')
implementation libs.dagger.hilt implementation libs.dagger.hilt
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'

View File

@ -187,9 +187,7 @@ dependencies {
// UI // UI
implementation libs.google.material implementation libs.google.material
api('me.gujun.android:span:1.7') { implementation project(":library:external:span")
exclude group: 'com.android.support', module: 'support-annotations'
}
implementation libs.markwon.core implementation libs.markwon.core
implementation libs.markwon.extLatex implementation libs.markwon.extLatex
implementation libs.markwon.imageGlide implementation libs.markwon.imageGlide