diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index 60808a0d7f..5505e34b25 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -240,7 +240,6 @@ ext.groups = [ ], group: [ 'me.dm7.barcodescanner', - 'me.gujun.android', ] ] ] diff --git a/library/external/jsonviewer/build.gradle b/library/external/jsonviewer/build.gradle index 7b3b62c082..c501f583d0 100644 --- a/library/external/jsonviewer/build.gradle +++ b/library/external/jsonviewer/build.gradle @@ -58,9 +58,7 @@ dependencies { implementation libs.airbnb.mavericks // Span utils - implementation('me.gujun.android:span:1.7') { - exclude group: 'com.android.support', module: 'support-annotations' - } + implementation project(":library:external:span") implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid diff --git a/library/external/span/build.gradle b/library/external/span/build.gradle new file mode 100644 index 0000000000..05adbacb4d --- /dev/null +++ b/library/external/span/build.gradle @@ -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' +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/span.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/span.kt new file mode 100644 index 0000000000..5ca63b7e01 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/span.kt @@ -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 = ArrayList() + + var style: Span = EMPTY_STYLE + + private fun buildCharacterStyle(builder: ArrayList) { + 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) { + 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() + 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) +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/CustomTypefaceSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/CustomTypefaceSpan.kt new file mode 100644 index 0000000000..57ae4548b6 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/CustomTypefaceSpan.kt @@ -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 + } +} \ No newline at end of file diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/LineSpacingSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/LineSpacingSpan.kt new file mode 100644 index 0000000000..53aa42aa83 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/LineSpacingSpan.kt @@ -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 + } + } +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/SimpleClickableSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/SimpleClickableSpan.kt new file mode 100644 index 0000000000..2ae524bca9 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/SimpleClickableSpan.kt @@ -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 + } +} diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/TextDecorationLineSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/TextDecorationLineSpan.kt new file mode 100644 index 0000000000..662cb82fed --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/TextDecorationLineSpan.kt @@ -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") + } + } +} \ No newline at end of file diff --git a/library/external/span/src/main/kotlin/me/gujun/android/span/style/VerticalPaddingSpan.kt b/library/external/span/src/main/kotlin/me/gujun/android/span/style/VerticalPaddingSpan.kt new file mode 100644 index 0000000000..600ea72e57 --- /dev/null +++ b/library/external/span/src/main/kotlin/me/gujun/android/span/style/VerticalPaddingSpan.kt @@ -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 + } + } +} diff --git a/settings.gradle b/settings.gradle index 7b57b530f7..bccfa7913a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,6 +14,7 @@ include ':library:external:dialpad' include ':library:external:textdrawable' include ':library:external:autocomplete' include ':library:external:realmfieldnameshelper' +include ':library:external:span' include ':library:rustCrypto' include ':matrix-sdk-android' diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 8221497e74..25980df834 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -396,6 +396,7 @@ dependencies { implementation project(':vector') implementation project(':vector-config') implementation project(':library:core-utils') + debugImplementation project(':library:external:span') debugImplementation project(':library:ui-styles') implementation libs.dagger.hilt implementation 'androidx.multidex:multidex:2.0.1' diff --git a/vector/build.gradle b/vector/build.gradle index 83f09fb10b..e45db64548 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -187,9 +187,7 @@ dependencies { // UI implementation libs.google.material - api('me.gujun.android:span:1.7') { - exclude group: 'com.android.support', module: 'support-annotations' - } + implementation project(":library:external:span") implementation libs.markwon.core implementation libs.markwon.extLatex implementation libs.markwon.imageGlide