[新增] 字典可显示图片 (@Luoyacheng)

This commit is contained in:
HapeLee
2025-12-24 00:17:07 +08:00
parent fe0856070f
commit 129cd83639
5 changed files with 187 additions and 5 deletions
@@ -0,0 +1,129 @@
package io.legado.app.help
import android.content.Context
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import android.os.Build
import android.text.Html
import android.widget.TextView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import java.lang.ref.WeakReference
class GlideImageGetter(private val context: Context, textView: TextView, private val originalHtml: String) :
Html.ImageGetter {
companion object {
fun create(context: Context, textView: TextView, html: String): GlideImageGetter {
return GlideImageGetter(context, textView, html)
}
private fun createEmptyDrawable(): Drawable {
return object : Drawable() {
override fun draw(canvas: android.graphics.Canvas) = Unit
override fun setAlpha(alpha: Int) = Unit
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int = PixelFormat.TRANSPARENT
}
}
}
private val loadedDrawables = mutableMapOf<String, Drawable>()
private val textViewRef = WeakReference(textView)
private val targets = mutableSetOf<CustomTarget<*>>()
override fun getDrawable(source: String?): Drawable {
if (source.isNullOrBlank()) {
return createEmptyDrawable()
}
val urlDrawable = GlideUrlDrawable()
val target = createImageTarget(urlDrawable, source)
targets.add(target)
Glide.with(context)
.load(source)
.into(target)
return urlDrawable
}
private fun createImageTarget(
urlDrawable: GlideUrlDrawable,
source: String
): CustomTarget<Drawable> {
return object : CustomTarget<Drawable>() {
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
targets.remove(this)
val textView = textViewRef.get() ?: return
val availableWidth = textView.width - textView.paddingLeft - textView.paddingRight
val maxWidth = availableWidth.takeIf { it > 0 } ?: 700
val drawableWidth = resource.intrinsicWidth.coerceAtLeast(1)
val drawableHeight = resource.intrinsicHeight.coerceAtLeast(1)
val scale = if (drawableWidth > maxWidth) {
maxWidth.toFloat() / drawableWidth
} else {
1f
}
val width = (drawableWidth * scale).toInt()
val height = (drawableHeight * scale).toInt()
resource.setBounds(0, 0, width, height)
urlDrawable.setDrawable(resource)
loadedDrawables[source] = urlDrawable
refreshTextView()
}
override fun onLoadCleared(placeholder: Drawable?) {
targets.remove(this)
urlDrawable.setDrawable(placeholder)
refreshTextView()
}
override fun onLoadFailed(errorDrawable: Drawable?) {
targets.remove(this)
val placeholder = errorDrawable ?: createEmptyDrawable()
urlDrawable.setDrawable(placeholder)
loadedDrawables[source] = urlDrawable
refreshTextView()
}
override fun onLoadStarted(placeholder: Drawable?) {
urlDrawable.setDrawable(placeholder)
}
}
}
fun clear() {
targets.forEach {
Glide.with(context).clear(it)
}
targets.clear()
textViewRef.clear()
}
private fun refreshTextView() {
val textView = textViewRef.get() ?: return
textView.post {
// 创建新的ImageGetter,但使用缓存
val cachedImageGetter = CachedImageGetter(loadedDrawables)
val newHtml =
Html.fromHtml(originalHtml, Html.FROM_HTML_MODE_COMPACT, cachedImageGetter, null)
textView.text = newHtml
}
}
private class CachedImageGetter(
private val drawableCache: Map<String, Drawable>
) : Html.ImageGetter {
override fun getDrawable(source: String?): Drawable {
if (source.isNullOrBlank()) {
return createEmptyDrawable()
}
drawableCache[source]?.let { return it }
return createEmptyDrawable().apply {
setBounds(0, 0, 1, 1)
}
}
}
}
@@ -0,0 +1,33 @@
package io.legado.app.help
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
class GlideUrlDrawable(): Drawable() {
private var mDrawable: Drawable? = null
fun setDrawable(drawable: Drawable?) {
this.mDrawable = drawable
drawable?.bounds?.let { bounds ->
setBounds(bounds)
}
invalidateSelf()
}
override fun draw(canvas: Canvas) {
mDrawable?.draw(canvas)
}
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
override fun setAlpha(alpha: Int) {
mDrawable?.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {
mDrawable?.colorFilter = colorFilter
}
}
@@ -9,6 +9,7 @@ import io.legado.app.R
import io.legado.app.base.BaseBottomSheetDialogFragment
import io.legado.app.data.entities.DictRule
import io.legado.app.databinding.DialogDictBinding
import io.legado.app.help.GlideImageGetter
import io.legado.app.utils.gone
import io.legado.app.utils.setHtml
import io.legado.app.utils.toastOnUi
@@ -28,8 +29,8 @@ class DictDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_dict) {
private val viewModel by viewModels<DictViewModel>()
private val binding by viewBinding(DialogDictBinding::bind)
private var word: String? = null
private var glideImageGetter: GlideImageGetter? = null
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.tvDict.movementMethod = LinkMovementMethod()
@@ -44,6 +45,12 @@ class DictDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_dict) {
observeDicts()
}
override fun onDestroyView() {
glideImageGetter?.clear()
glideImageGetter = null
super.onDestroyView()
}
private fun setupTabs() {
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab) = Unit
@@ -90,6 +97,9 @@ class DictDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_dict) {
} else {
binding.tvDict.visible()
binding.tvDict.setHtml(result)
glideImageGetter?.clear()
glideImageGetter = GlideImageGetter.create(requireContext(), binding.tvDict, result)
binding.tvDict.setHtml(result, glideImageGetter)
}
}
}
@@ -5,6 +5,7 @@ import android.os.Bundle
import android.view.View
import android.view.textclassifier.TextClassifier
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import io.legado.app.R
import io.legado.app.base.BaseBottomSheetDialogFragment
import io.legado.app.databinding.DialogTextViewBinding
@@ -66,13 +67,11 @@ class TextDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_text_view) {
val content = IntentData.get(it.getString("content")) ?: ""
when (it.getString("mode")) {
Mode.MD.name -> viewLifecycleOwner.lifecycleScope.launch {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
binding.textView.setTextClassifier(TextClassifier.NO_OP)
}
binding.textView.setTextClassifier(TextClassifier.NO_OP)
val markwon: Markwon
val markdown = withContext(IO) {
markwon = Markwon.builder(requireContext())
.usePlugin(GlideImagesPlugin.create(requireContext()))
.usePlugin(GlideImagesPlugin.create(Glide.with(requireContext())))
.usePlugin(HtmlPlugin.create())
.usePlugin(TablePlugin.create(requireContext()))
.build()
@@ -39,6 +39,7 @@ import androidx.core.view.marginBottom
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import io.legado.app.help.GlideImageGetter
import io.legado.app.help.config.AppConfig
import io.legado.app.lib.theme.TintHelper
import io.legado.app.utils.canvasrecorder.CanvasRecorder
@@ -234,6 +235,16 @@ fun TextView.setHtml(html: String) {
}
}
@SuppressLint("ObsoleteSdkInt")
fun TextView.setHtml(html: String, imageGetter: GlideImageGetter?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
text = Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT, imageGetter, null)
} else {
@Suppress("DEPRECATION")
text = Html.fromHtml(html, imageGetter, null)
}
}
fun TextView.setTextIfNotEqual(charSequence: CharSequence?) {
if (text != charSequence) {
text = charSequence