From 507657f273a4b3efb67aa9212c578dee36a55ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Tue, 17 Mar 2020 16:05:21 +0100 Subject: [PATCH] [UI/Messages] Improve HTML lists presentation. --- .../ui/modules/messages/MessagesUtils.kt | 49 +++------- .../edziennik/utils/html/BetterHtml.kt | 89 +++++++++++++++++++ .../utils/html/ImprovedBulletSpan.kt | 79 ++++++++++++++++ .../edziennik/utils/html/LiTagHandler.kt | 45 ++++++++++ 4 files changed, 223 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/utils/html/ImprovedBulletSpan.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/utils/html/LiTagHandler.kt diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt index 4808d2ef..8a5549bd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt @@ -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) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt new file mode 100644 index 00000000..cca59802 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt @@ -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 = """
    (.+?)""" + .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 + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/ImprovedBulletSpan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/ImprovedBulletSpan.kt new file mode 100644 index 00000000..96dcbdfa --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/ImprovedBulletSpan.kt @@ -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 + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/LiTagHandler.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/LiTagHandler.kt new file mode 100644 index 00000000..36f58d03 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/LiTagHandler.kt @@ -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