[UI/Messages] Improve HTML lists presentation.

This commit is contained in:
Kuba Szczodrzyński 2020-03-17 16:05:21 +01:00
parent 60641742ed
commit 507657f273
4 changed files with 223 additions and 39 deletions

View File

@ -1,17 +1,21 @@
package pl.szczodrzynski.edziennik.ui.modules.messages
import android.content.Context
import android.graphics.*
import android.os.Build
import android.text.Html
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.text.Spanned
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.full.MessageFull
import pl.szczodrzynski.edziennik.fixName
import pl.szczodrzynski.edziennik.getNameInitials
import pl.szczodrzynski.edziennik.utils.Colors
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.navlib.blendColors
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import kotlin.math.roundToInt
object MessagesUtils {
@ -179,39 +183,6 @@ object MessagesUtils {
@JvmStatic
fun htmlToSpannable(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))
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)
} else {
Html.fromHtml(text)
}
return BetterHtml.fromHtml(context, html)
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}
}
}
}