Skip to content

Instantly share code, notes, and snippets.

@alexzaitsev
Last active January 17, 2024 08:45
Show Gist options
  • Save alexzaitsev/060fc607d5a16eeef17276bb57e0914f to your computer and use it in GitHub Desktop.
Save alexzaitsev/060fc607d5a16eeef17276bb57e0914f to your computer and use it in GitHub Desktop.
How to display HTML using Android Compose
/**
* Set this on a textview and then you can potentially open links locally if applicable
*/
public class DefaultLinkMovementMethod extends LinkMovementMethod {
private OnLinkClickedListener mOnLinkClickedListener;
public DefaultLinkMovementMethod(OnLinkClickedListener onLinkClickedListener) {
mOnLinkClickedListener = onLinkClickedListener;
}
public boolean onTouchEvent(TextView widget, android.text.Spannable buffer, android.view.MotionEvent event) {
int action = event.getAction();
//http://stackoverflow.com/questions/1697084/handle-textview-link-click-in-my-android-app
if (action == MotionEvent.ACTION_UP) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
URLSpan[] link = buffer.getSpans(off, off, URLSpan.class);
if (link.length != 0) {
String url = link[0].getURL();
boolean handled = mOnLinkClickedListener.onLinkClicked(url);
if (handled) {
return true;
}
return super.onTouchEvent(widget, buffer, event);
}
}
return super.onTouchEvent(widget, buffer, event);
}
public interface OnLinkClickedListener {
boolean onLinkClicked(String url);
}
}
fun fromHtml(context: Context, html: String): Spannable = parse(html).apply {
removeLinksUnderline()
styleBold(context)
}
private fun parse(html: String): Spannable =
(HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT) as Spannable)
private fun Spannable.removeLinksUnderline() {
for (s in getSpans(0, length, URLSpan::class.java)) {
setSpan(object : UnderlineSpan() {
override fun updateDrawState(tp: TextPaint) {
tp.isUnderlineText = false
}
}, getSpanStart(s), getSpanEnd(s), 0)
}
}
private fun Spannable.styleBold(context: Context) {
val bold = ResourcesCompat.getFont(context, R.font.inter_medium)!!
for (s in getSpans(0, length, StyleSpan::class.java)) {
if (s.style == Typeface.BOLD) {
setSpan(ForegroundColorSpan(Color.BLACK), getSpanStart(s), getSpanEnd(s), 0)
setSpan(bold.getTypefaceSpan(), getSpanStart(s), getSpanEnd(s), 0)
}
}
}
private const val LINK_1 = "link_1"
private const val LINK_2 = "link_2"
private const val SPACING_FIX = 3f
@Composable
fun HtmlText(
modifier: Modifier = Modifier,
html: String,
textStyle: TextStyle = Typography.body1,
onLink1Clicked: (() -> Unit)? = null,
onLink2Clicked: (() -> Unit)? = null
) {
AndroidView(
modifier = modifier,
update = { it.text = fromHtml(it.context, html) },
factory = { context ->
val spacingReady =
max(textStyle.lineHeight.value - textStyle.fontSize.value - SPACING_FIX, 0f)
val extraSpacing = spToPx(spacingReady.toInt(), context)
val gravity = when (textStyle.textAlign) {
TextAlign.Center -> Gravity.CENTER
TextAlign.End -> Gravity.END
else -> Gravity.START
}
val fontResId = when (textStyle.fontWeight) {
FontWeight.Medium -> R.font.inter_medium
else -> R.font.inter_regular
}
val font = ResourcesCompat.getFont(context, fontResId)
TextView(context).apply {
// general style
textSize = textStyle.fontSize.value
setLineSpacing(extraSpacing, 1f)
setTextColor(textStyle.color.toArgb())
setGravity(gravity)
typeface = font
// links
setLinkTextColor(Primary.toArgb())
movementMethod = DefaultLinkMovementMethod() { link ->
when (link) {
LINK_1 -> onLink1Clicked?.invoke()
LINK_2 -> onLink2Clicked?.invoke()
}
true
}
}
}
)
}
fun spToPx(sp: Int, context: Context): Float =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
sp.toFloat(),
context.resources.displayMetrics
)
fun Typeface.getTypefaceSpan(): MetricAffectingSpan =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
typefaceSpanCompatV28(this)
} else {
CustomTypefaceSpan(this)
}
@TargetApi(Build.VERSION_CODES.P)
private fun typefaceSpanCompatV28(typeface: Typeface) = TypefaceSpan(typeface)
private class CustomTypefaceSpan(private val typeface: Typeface?) : MetricAffectingSpan() {
override fun updateDrawState(paint: TextPaint) {
paint.typeface = typeface
}
override fun updateMeasureState(paint: TextPaint) {
paint.typeface = typeface
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment