Revert "Kill Legacy TextView (#1786)"

This reverts commit 62927cb6

Reason for revert: Bad performance
This commit is contained in:
FooIbar committed 2024-12-08 00:07:21 +08:00
1 parent 31be546e8e
commit bd246300e0
5 files changed
+155 -113

No files matched your search

@@ -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()
}
},
)
}
}
}
@@ -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<URLSpan>(off, off)
if (spans.isNotEmpty()) {
onUrlClick(spans[0].url)
}
}
}
}
return super.onTouchEvent(event)
}
}
@@ -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<Pair<String, String>, AnnotatedString>
private const val IMAGE_OBJ = ''
private val IMAGE_PATTERN = Regex("(?:<a href=\"([^\"]+)\">)?<img src=\"([^\"]+)\"")
fun breakToTextAndUrls(origin: String, text: AnnotatedString): List<TextOrUrls> {
val urls = IMAGE_PATTERN.findAll(origin).toList()
if (urls.isEmpty()) return listOf(text.right())
val iter = urls.iterator()
var currentOfs = 0
return buildList<TextOrUrls> {
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
}
}
}
@@ -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<URLSpan>(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) {
@@ -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(