From bd246300e00fb5e3f8da74795d31cffa69b47a82 Mon Sep 17 00:00:00 2001 From: FooIbar <118464521+FooIbar@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:21:28 +0800 Subject: [PATCH] Revert "Kill Legacy TextView (#1786)" This reverts commit 62927cb6 Reason for revert: Bad performance --- .../ehviewer/ui/legacy/CoilImageGetter.kt | 40 ++++++ .../ehviewer/ui/legacy/LinkifyTextView.kt | 51 +++++++ .../hippo/ehviewer/ui/main/GalleryComment.kt | 126 +++++------------- .../ui/screen/GalleryCommentsScreen.kt | 47 ++++--- .../ui/screen/GalleryDetailContent.kt | 4 +- 5 files changed, 155 insertions(+), 113 deletions(-) create mode 100644 app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/CoilImageGetter.kt create mode 100644 app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/LinkifyTextView.kt diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/CoilImageGetter.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/CoilImageGetter.kt new file mode 100644 index 000000000..f61a69873 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/CoilImageGetter.kt @@ -0,0 +1,40 @@ +package com.hippo.ehviewer.ui.legacy + +import android.graphics.drawable.DrawableWrapper +import android.text.Html +import coil3.asDrawable +import coil3.decode.DecodeUtils +import coil3.imageLoader +import coil3.request.crossfade +import coil3.size.Scale +import com.hippo.ehviewer.ktbuilder.imageRequest +import com.hippo.ehviewer.util.toIntOrDefault +import kotlin.math.roundToInt +import splitties.init.appCtx + +private val UrlRegex = "(?:[^-]+-){2}(\\d+)-(\\d+)-[^_]+_([^.]+)".toRegex() + +class CoilImageGetter(private val onSuccess: () -> Unit) : Html.ImageGetter { + override fun getDrawable(source: String) = object : DrawableWrapper(null) {}.apply { + UrlRegex.find(source)?.run { + val srcWidth = groupValues[1].toInt() + val srcHeight = groupValues[2].toInt() + val dstWidth = groupValues[3].toIntOrDefault(200) + val dstHeight = dstWidth / 2 * 3 + val multiplier = DecodeUtils.computeSizeMultiplier(srcWidth, srcHeight, dstWidth, dstHeight, Scale.FIT) + setBounds(0, 0, (srcWidth * multiplier).roundToInt(), (srcHeight * multiplier).roundToInt()) + } ?: setBounds(0, 0, 200, 300) + with(appCtx) { + imageLoader.enqueue( + imageRequest { + data(source) + crossfade(false) + target { image -> + setDrawable(image.asDrawable(resources)) + onSuccess() + } + }, + ) + } + } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/LinkifyTextView.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/LinkifyTextView.kt new file mode 100644 index 000000000..82dcf6882 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/legacy/LinkifyTextView.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2015 Hippo Seven + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hippo.ehviewer.ui.legacy + +import android.content.Context +import android.text.Spanned +import android.text.style.URLSpan +import android.view.MotionEvent +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.text.getSpans + +class LinkifyTextView( + context: Context, + private val onUrlClick: (String) -> Unit, +) : AppCompatTextView(context) { + override fun onTouchEvent(event: MotionEvent): Boolean { + // Let the parent or grandparent of TextView to handles click aciton. + // Otherwise click effect like ripple will not work, and if touch area + // do not contain a url, the TextView will still get MotionEvent. + // onTouchEven must be called with MotionEvent.ACTION_DOWN for each touch + // action on it, so we analyze touched url here. + if (event.action == MotionEvent.ACTION_DOWN) { + (text as? Spanned)?.let { text -> + layout?.let { layout -> + val x = event.x + scrollX - totalPaddingLeft + val y = event.y + scrollY - totalPaddingTop + val line = layout.getLineForVertical(y.toInt()) + val off = layout.getOffsetForHorizontal(line, x.toFloat()) + val spans = text.getSpans(off, off) + if (spans.isNotEmpty()) { + onUrlClick(spans[0].url) + } + } + } + } + return super.onTouchEvent(event) + } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryComment.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryComment.kt index 3b1986c18..2d1111d28 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryComment.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryComment.kt @@ -1,57 +1,34 @@ package com.hippo.ehviewer.ui.main +import android.text.Html +import android.text.TextUtils import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Card +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.neverEqualPolicy +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.LinkAnnotation -import androidx.compose.ui.text.LinkInteractionListener -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextLinkStyles -import androidx.compose.ui.text.fromHtml -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import arrow.core.Either -import arrow.core.left -import arrow.core.right -import coil3.compose.AsyncImage +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.text.parseAsHtml import com.hippo.ehviewer.R import com.hippo.ehviewer.client.data.GalleryComment -import com.hippo.ehviewer.ui.tools.thenIf +import com.hippo.ehviewer.ui.legacy.CoilImageGetter +import com.hippo.ehviewer.ui.legacy.LinkifyTextView import com.hippo.ehviewer.util.ReadableTime -typealias TextOrUrls = Either, AnnotatedString> - -private const val IMAGE_OBJ = '' -private val IMAGE_PATTERN = Regex("(?:)? { - val urls = IMAGE_PATTERN.findAll(origin).toList() - if (urls.isEmpty()) return listOf(text.right()) - val iter = urls.iterator() - var currentOfs = 0 - return buildList { - while (true) { - val index = text.text.indexOf(IMAGE_OBJ, currentOfs) - if (index == -1) break - add(text.subSequence(currentOfs, index).right()) - add(iter.next().groupValues.let { it[1] to it[2] }.left()) - currentOfs = index + 1 - } - add(text.subSequence(currentOfs, text.length).right()) - } -} - @Composable fun GalleryCommentCard( modifier: Modifier = Modifier, @@ -60,72 +37,43 @@ fun GalleryCommentCard( onUserClick: () -> Unit, onUrlClick: (String) -> Unit, maxLines: Int = Int.MAX_VALUE, - overflow: TextOverflow = TextOverflow.Clip, - showImage: Boolean = false, - processComment: @Composable (GalleryComment, TextLinkStyles, LinkInteractionListener) -> AnnotatedString = { c, s, l -> AnnotatedString.fromHtml(c.comment, s, l) }, + ellipsis: Boolean = false, + processComment: @Composable (GalleryComment, Html.ImageGetter) -> CharSequence = { c, ig -> c.comment.parseAsHtml(imageGetter = ig) }, ) = with(comment) { Card(onClick = onCardClick, modifier = modifier) { val margin = dimensionResource(id = R.dimen.keyline_margin) - ConstraintLayout(modifier = Modifier.padding(horizontal = margin, vertical = 8.dp).fillMaxWidth()) { - val (userRef, timeRef, commentRef) = createRefs() + Row( + modifier = Modifier.padding(horizontal = margin, vertical = 8.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { ProvideTextStyle(value = MaterialTheme.typography.titleSmall) { val userText = if (uploader) stringResource(id = R.string.comment_user_uploader, user.orEmpty()) else user.orEmpty() Text( text = userText, - modifier = Modifier.constrainAs(userRef) { - top.linkTo(parent.top) - start.linkTo(parent.start) - }.clickable(onClick = onUserClick), + modifier = Modifier.clickable(onClick = onUserClick), ) Text( text = ReadableTime.getTimeAgo(time), - modifier = Modifier.constrainAs(timeRef) { - top.linkTo(parent.top) - end.linkTo(parent.end) - }, ) } - Column( - modifier = Modifier.constrainAs(commentRef) { - start.linkTo(parent.start) - top.linkTo(userRef.bottom, margin = 8.dp) - }, - ) { - ProvideTextStyle(value = MaterialTheme.typography.bodyMedium) { - val linkColor = MaterialTheme.colorScheme.primary - val processed = processComment( - comment, - TextLinkStyles(style = SpanStyle(color = linkColor)), - ) { link -> - check(link is LinkAnnotation.Url) - onUrlClick(link.url) - } - if (showImage) { - val list = breakToTextAndUrls(comment.comment, processed) - list.forEach { - when (it) { - is Either.Left -> { - val (url, imageUrl) = it.value - AsyncImage( - model = imageUrl, - contentDescription = null, - modifier = Modifier.thenIf(url.isNotEmpty()) { - clickable { onUrlClick(url) } - }, - ) - } - is Either.Right -> Text(text = it.value) - } - } - } else { - Text( - text = processed, - maxLines = maxLines, - overflow = overflow, - ) - } + } + val textColor = LocalContentColor.current.toArgb() + val linkTextColor = MaterialTheme.colorScheme.primary.toArgb() + val redrawSignal = remember { mutableStateOf(Unit, neverEqualPolicy()) } + val commentText = processComment(comment, CoilImageGetter { redrawSignal.value = Unit }) + AndroidView( + factory = { context -> + LinkifyTextView(context, onUrlClick).apply { + setTextColor(textColor) + setLinkTextColor(linkTextColor) + this.maxLines = maxLines + if (ellipsis) ellipsize = TextUtils.TruncateAt.END } - } + }, + modifier = Modifier.padding(start = margin, end = margin, bottom = 8.dp), + ) { view -> + view.text = commentText + redrawSignal.value } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryCommentsScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryCommentsScreen.kt index 62c74a702..3481a6a6e 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryCommentsScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryCommentsScreen.kt @@ -1,5 +1,12 @@ package com.hippo.ehviewer.ui.screen +import android.graphics.Typeface +import android.text.Html +import android.text.Spannable +import android.text.style.ForegroundColorSpan +import android.text.style.RelativeSizeSpan +import android.text.style.StyleSpan +import android.text.style.URLSpan import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Arrangement @@ -50,6 +57,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.onGloballyPositioned @@ -60,20 +68,15 @@ import androidx.compose.ui.platform.LocalTextToolbar import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.LinkAnnotation -import androidx.compose.ui.text.LinkInteractionListener -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextLinkStyles -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.em import androidx.compose.ui.unit.max import androidx.compose.ui.util.lerp +import androidx.core.text.buildSpannedString +import androidx.core.text.getSpans +import androidx.core.text.inSpans import androidx.core.text.parseAsHtml import com.hippo.ehviewer.R import com.hippo.ehviewer.Settings @@ -105,8 +108,8 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.logcat +import kotlin.collections.forEach import kotlin.math.roundToInt -import kotlin.sequences.forEach import kotlinx.coroutines.launch import moe.tarsin.coroutines.runSuspendCatching @@ -115,31 +118,34 @@ private val URL_PATTERN = Regex("(http|https)://[a-z0-9A-Z%-]+(\\.[a-z0-9A-Z%-]+ @Composable fun processComment( comment: GalleryComment, - linkStyle: TextLinkStyles, - onLinkClick: LinkInteractionListener, -) = AnnotatedString.fromHtml(comment.comment, linkStyle, onLinkClick).let { text -> - buildAnnotatedString { - val onSurfaceVariant = MaterialTheme.colorScheme.onSurfaceVariant + imageGetter: Html.ImageGetter, +) = comment.comment.parseAsHtml(imageGetter = imageGetter).let { text -> + buildSpannedString { append(text) URL_PATTERN.findAll(text).forEach { result -> val start = result.range.first val end = result.range.last + 1 - if (!text.hasLinkAnnotations(start, end)) { - addLink(LinkAnnotation.Url(result.groupValues[0], linkStyle, onLinkClick), start, end) + if (getSpans(start, end).isEmpty()) { + setSpan(URLSpan(result.groupValues[0]), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } } - val style = SpanStyle(fontSize = 0.8f.em, fontWeight = FontWeight.Bold, color = onSurfaceVariant) + val color = MaterialTheme.colorScheme.onSurfaceVariant.toArgb() + val spans = arrayOf( + RelativeSizeSpan(0.8f), + StyleSpan(Typeface.BOLD), + ForegroundColorSpan(color), + ) if (comment.id != 0L && comment.score != 0) { val score = comment.score val scoreString = if (score > 0) "+$score" else score.toString() append(" ") - withStyle(style) { + inSpans(*spans) { append(scoreString) } } if (comment.lastEdited != 0L) { append("\n\n") - withStyle(style) { + inSpans(*spans) { append( stringResource( R.string.last_edited, @@ -386,8 +392,7 @@ fun AnimatedVisibilityScope.GalleryCommentsScreen(gid: Long, navigator: Destinat }, onCardClick = { launch { doCommentAction(item) } }, onUrlClick = { if (!jumpToReaderByPage(it, galleryDetail)) if (!navWithUrl(it)) openBrowser(it) }, - processComment = { c, s, l -> processComment(c, s, l) }, - showImage = true, + processComment = { c, ig -> processComment(c, ig) }, ) } if (comments.hasMore) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailContent.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailContent.kt index 32abfdc1c..3ad77dd70 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailContent.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailContent.kt @@ -51,7 +51,6 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.LocalPinnableContainer import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.viewModelScope @@ -417,8 +416,7 @@ fun BelowHeader(galleryDetail: GalleryDetail, voteTag: VoteTag) { onUserClick = ::onNavigateToCommentScene, onUrlClick = { if (!jumpToReaderByPage(it, galleryDetail)) if (!navWithUrl(it)) openBrowser(it) }, maxLines = 5, - overflow = TextOverflow.Ellipsis, - showImage = false, + ellipsis = true, ) } Box(