mirror of
https://github.com/szkolny-eu/szkolny-android.git
synced 2025-01-31 05:48:19 +01:00
[UI/Messages] Improve HTML lists presentation.
This commit is contained in:
parent
60641742ed
commit
507657f273
@ -1,17 +1,21 @@
|
|||||||
package pl.szczodrzynski.edziennik.ui.modules.messages
|
package pl.szczodrzynski.edziennik.ui.modules.messages
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.*
|
import android.graphics.Bitmap
|
||||||
import android.os.Build
|
import android.graphics.Canvas
|
||||||
import android.text.Html
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import pl.szczodrzynski.edziennik.*
|
import pl.szczodrzynski.edziennik.App
|
||||||
|
import pl.szczodrzynski.edziennik.R
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.Message
|
import pl.szczodrzynski.edziennik.data.db.entity.Message
|
||||||
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||||
|
import pl.szczodrzynski.edziennik.fixName
|
||||||
|
import pl.szczodrzynski.edziennik.getNameInitials
|
||||||
import pl.szczodrzynski.edziennik.utils.Colors
|
import pl.szczodrzynski.edziennik.utils.Colors
|
||||||
import pl.szczodrzynski.edziennik.utils.Utils
|
import pl.szczodrzynski.edziennik.utils.Utils
|
||||||
import pl.szczodrzynski.navlib.blendColors
|
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
object MessagesUtils {
|
object MessagesUtils {
|
||||||
@ -179,39 +183,6 @@ object MessagesUtils {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun htmlToSpannable(context: Context, html: String): Spanned {
|
fun htmlToSpannable(context: Context, html: String): Spanned {
|
||||||
val hexPattern = "(#[a-fA-F0-9]{6})"
|
return BetterHtml.fromHtml(context, html)
|
||||||
val colorRegex = "(?:color=\"$hexPattern\")|(?:style=\"color: ?${hexPattern})"
|
|
||||||
.toRegex(RegexOption.IGNORE_CASE)
|
|
||||||
|
|
||||||
var text = html
|
|
||||||
.replace("\\[META:[A-z0-9]+;[0-9-]+]".toRegex(), "")
|
|
||||||
.replace("background-color: ?$hexPattern;".toRegex(), "")
|
|
||||||
|
|
||||||
val colorBackground = android.R.attr.colorBackground.resolveAttr(context)
|
|
||||||
val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(context) and 0xffffff
|
|
||||||
|
|
||||||
colorRegex.findAll(text).forEach { result ->
|
|
||||||
val group = result.groups.drop(1).firstOrNull { it != null } ?: return@forEach
|
|
||||||
|
|
||||||
val color = Color.parseColor(group.value)
|
|
||||||
var newColor = 0xff000000.toInt() or color
|
|
||||||
|
|
||||||
var blendAmount = 1
|
|
||||||
var numIterations = 0
|
|
||||||
|
|
||||||
while (numIterations < 100 && ColorUtils.calculateContrast(colorBackground, newColor) < 4.5f) {
|
|
||||||
blendAmount += 2
|
|
||||||
newColor = blendColors(color, blendAmount shl 24 or textColorPrimary)
|
|
||||||
numIterations++
|
|
||||||
}
|
|
||||||
|
|
||||||
text = text.replaceRange(group.range, "#" + (newColor and 0xffffff).toString(16))
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)
|
|
||||||
} else {
|
|
||||||
Html.fromHtml(text)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2020-3-17.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pl.szczodrzynski.edziennik.utils.html
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.BulletSpan
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
|
import pl.szczodrzynski.edziennik.dp
|
||||||
|
import pl.szczodrzynski.edziennik.resolveAttr
|
||||||
|
import pl.szczodrzynski.navlib.blendColors
|
||||||
|
|
||||||
|
object BetterHtml {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun fromHtml(context: Context, html: String): Spanned {
|
||||||
|
val hexPattern = "(#[a-fA-F0-9]{6})"
|
||||||
|
val colorRegex = "(?:color=\"$hexPattern\")|(?:style=\"color: ?${hexPattern})"
|
||||||
|
.toRegex(RegexOption.IGNORE_CASE)
|
||||||
|
|
||||||
|
var text = html
|
||||||
|
.replace("\\[META:[A-z0-9]+;[0-9-]+]".toRegex(), "")
|
||||||
|
.replace("background-color: ?$hexPattern;".toRegex(), "")
|
||||||
|
|
||||||
|
val colorBackground = android.R.attr.colorBackground.resolveAttr(context)
|
||||||
|
val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(context) and 0xffffff
|
||||||
|
|
||||||
|
colorRegex.findAll(text).forEach { result ->
|
||||||
|
val group = result.groups.drop(1).firstOrNull { it != null } ?: return@forEach
|
||||||
|
|
||||||
|
val color = Color.parseColor(group.value)
|
||||||
|
var newColor = 0xff000000.toInt() or color
|
||||||
|
|
||||||
|
var blendAmount = 1
|
||||||
|
var numIterations = 0
|
||||||
|
|
||||||
|
while (numIterations < 100 && ColorUtils.calculateContrast(colorBackground, newColor) < 4.5f) {
|
||||||
|
blendAmount += 2
|
||||||
|
newColor = blendColors(color, blendAmount shl 24 or textColorPrimary)
|
||||||
|
numIterations++
|
||||||
|
}
|
||||||
|
|
||||||
|
text = text.replaceRange(group.range, "#" + (newColor and 0xffffff).toString(16))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*val olRegex = """<ol>(.+?)</\s*?ol>"""
|
||||||
|
.toRegex(setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.IGNORE_CASE))
|
||||||
|
olRegex.findAll(text).forEach {
|
||||||
|
text.replaceRange(
|
||||||
|
it.range,
|
||||||
|
text.slice(it.range).replace("li>", "_li>")
|
||||||
|
)
|
||||||
|
}*/
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val htmlSpannable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
Html.fromHtml(
|
||||||
|
text,
|
||||||
|
Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM or Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST or Html.FROM_HTML_SEPARATOR_LINE_BREAK_DIV,
|
||||||
|
null,
|
||||||
|
LiTagHandler()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Html.fromHtml(text, null, LiTagHandler())
|
||||||
|
}
|
||||||
|
|
||||||
|
val spannableBuilder = SpannableStringBuilder(htmlSpannable)
|
||||||
|
val bulletSpans = spannableBuilder.getSpans(0, spannableBuilder.length, BulletSpan::class.java)
|
||||||
|
bulletSpans.forEach {
|
||||||
|
val start = spannableBuilder.getSpanStart(it)
|
||||||
|
val end = spannableBuilder.getSpanEnd(it)
|
||||||
|
spannableBuilder.removeSpan(it)
|
||||||
|
spannableBuilder.setSpan(
|
||||||
|
ImprovedBulletSpan(bulletRadius = 3.dp, startWidth = 24.dp, gapWidth = 8.dp),
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return spannableBuilder
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2020-3-17.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/davidbilik/bullet-span-sample/blob/master/app/src/main/java/cz/davidbilik/bulletsample/ImprovedBulletSpan.kt
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pl.szczodrzynski.edziennik.utils.html
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.Path.Direction
|
||||||
|
import android.text.Layout
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.LeadingMarginSpan
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy of [android.text.style.BulletSpan] from android SDK 28 with removed internal code
|
||||||
|
*/
|
||||||
|
class ImprovedBulletSpan(
|
||||||
|
val bulletRadius: Int = STANDARD_BULLET_RADIUS,
|
||||||
|
val startWidth: Int = STANDARD_GAP_WIDTH,
|
||||||
|
val gapWidth: Int = STANDARD_GAP_WIDTH,
|
||||||
|
val color: Int = STANDARD_COLOR
|
||||||
|
) : LeadingMarginSpan {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Bullet is slightly bigger to avoid aliasing artifacts on mdpi devices.
|
||||||
|
private const val STANDARD_BULLET_RADIUS = 4
|
||||||
|
private const val STANDARD_GAP_WIDTH = 2
|
||||||
|
private const val STANDARD_COLOR = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mBulletPath: Path? = null
|
||||||
|
|
||||||
|
override fun getLeadingMargin(first: Boolean): Int {
|
||||||
|
return startWidth + 2 * bulletRadius + gapWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun drawLeadingMargin(
|
||||||
|
canvas: Canvas, paint: Paint, x: Int, dir: Int,
|
||||||
|
top: Int, baseline: Int, bottom: Int,
|
||||||
|
text: CharSequence, start: Int, end: Int,
|
||||||
|
first: Boolean,
|
||||||
|
layout: Layout?
|
||||||
|
) {
|
||||||
|
if ((text as Spanned).getSpanStart(this) == start) {
|
||||||
|
val style = paint.style
|
||||||
|
paint.style = Paint.Style.FILL
|
||||||
|
|
||||||
|
val yPosition = if (layout != null) {
|
||||||
|
val line = layout.getLineForOffset(start)
|
||||||
|
layout.getLineBaseline(line).toFloat() - bulletRadius * 2f
|
||||||
|
} else {
|
||||||
|
(top + bottom) / 2f
|
||||||
|
}
|
||||||
|
|
||||||
|
val xPosition = startWidth + (x + dir * bulletRadius).toFloat()
|
||||||
|
|
||||||
|
if (canvas.isHardwareAccelerated) {
|
||||||
|
if (mBulletPath == null) {
|
||||||
|
mBulletPath = Path()
|
||||||
|
mBulletPath!!.addCircle(0.0f, 0.0f, bulletRadius.toFloat(), Direction.CW)
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.save()
|
||||||
|
canvas.translate(xPosition, yPosition)
|
||||||
|
canvas.drawPath(mBulletPath!!, paint)
|
||||||
|
canvas.restore()
|
||||||
|
} else {
|
||||||
|
canvas.drawCircle(xPosition, yPosition, bulletRadius.toFloat(), paint)
|
||||||
|
}
|
||||||
|
|
||||||
|
paint.style = style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) Kuba Szczodrzyński 2020-3-17.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/davidbilik/bullet-span-sample/blob/master/app/src/main/java/cz/davidbilik/bulletsample/LiTagHandler.kt
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pl.szczodrzynski.edziennik.utils.html
|
||||||
|
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.BulletSpan
|
||||||
|
import org.xml.sax.XMLReader
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Html.TagHandler] implementation that processes <ul> and <li> tags and creates bullets.
|
||||||
|
*
|
||||||
|
* Note: This class is only applied on SDK < 25 and processes only one-level list, nested lists do not work.
|
||||||
|
*/
|
||||||
|
class LiTagHandler : Html.TagHandler {
|
||||||
|
/**
|
||||||
|
* Helper marker class. Idea stolen from [Html.fromHtml] implementation
|
||||||
|
*/
|
||||||
|
class Bullet
|
||||||
|
|
||||||
|
override fun handleTag(opening: Boolean, tag: String, output: Editable, xmlReader: XMLReader) {
|
||||||
|
if (tag == "li" && opening) {
|
||||||
|
output.setSpan(Bullet(), output.length, output.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
if (tag == "li" && !opening) {
|
||||||
|
output.append("\n")
|
||||||
|
val lastMark = output.getSpans(0, output.length, Bullet::class.java).lastOrNull()
|
||||||
|
lastMark?.let {
|
||||||
|
val start = output.getSpanStart(it)
|
||||||
|
output.removeSpan(it)
|
||||||
|
if (start != output.length) {
|
||||||
|
output.setSpan(BulletSpan(), start, output.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user