You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android TextView含图文HTML点击放大后图片不显示问题求助

问题描述

我有一段包含文本和图片的HTML字符串,在TextView上渲染显示正常,示例HTML如下:

HTML字符串

<span style="font-family:;">What is the major product of the following reaction?</span><img src="https://example.com/image7.png"/>

点击TextView时,我想在白色背景上放大显示内容,目前文本放大功能正常,但图片无法显示。以下是我的实现代码:

点击事件代码

binding.tvQuestion.setOnClickListener {
        Intent(requireContext, FullViewImageActivity::class.java).also {
            it.putExtra("text", binding.tvQuestion.text.toString())
            startActivity(it)
        }
    }

FullViewImageActivity代码

class FullViewImageActivity : AppCompatActivity() {
lateinit var binding: ActivityFullViewImageBinding

private var text: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityFullViewImageBinding.inflate(layoutInflater)
    setContentView(binding.root)
    text = intent.extras!!.getString("text")

    if (!TextUtils.isEmpty(text)) {
        binding.imageView.setImageBitmap(
            drawMultilineTextToBitmap(
                this,
                R.drawable.white,
                text
            )
        )
    } else {
        Toast.makeText(this, "Text is empty", Toast.LENGTH_SHORT).show()
    }
}

fun drawMultilineTextToBitmap(
    gContext: Context,
    gResId: Int,
    gText: String?
): Bitmap? {

    // prepare canvas
    val resources = gContext.resources
    val scale = resources.displayMetrics.density
    var bitmap = BitmapFactory.decodeResource(resources, gResId)
    var bitmapConfig = bitmap.config
    // set default bitmap config if none
    if (bitmapConfig == null) {
        bitmapConfig = Bitmap.Config.ARGB_8888
    }
    // resource bitmaps are imutable,
    // so we need to convert it to mutable one
    bitmap = bitmap.copy(bitmapConfig, true)
    val canvas = Canvas(bitmap)

    // new antialiased Paint
    val paint = TextPaint(Paint.ANTI_ALIAS_FLAG)
    // text color - #3D3D3D
    paint.color = Color.rgb(61, 61, 61)
    // text size in pixels
    paint.textSize = (14 * scale).toInt().toFloat()
    // text shadow
    paint.setShadowLayer(1f, 0f, 1f, Color.WHITE)

    // set text width to canvas width minus 16dp padding
    val textWidth = canvas.width - (16 * scale).toInt()

    // init StaticLayout for text
    val textLayout = StaticLayout(
        gText, paint, textWidth, Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false
    )

    // get height of multiline text
    val textHeight = textLayout.height

    // get position of text's top left corner
    val x = ((bitmap.width - textWidth) / 2).toFloat()
    val y = ((bitmap.height - textHeight) / 2).toFloat()

    // draw text to the Canvas center
    canvas.save()
    canvas.translate(x, y)
    textLayout.draw(canvas)
    canvas.restore()
    return bitmap
}

布局XML代码

<LinearLayout
    android:id="@+id/mainLay"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    android:orientation="vertical">

    <com.app.trigerexam.exam.ui.tvzoom.TouchImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitCenter" />

</LinearLayout>

TouchImageView代码

public class TouchImageView extends AppCompatImageView implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {

Matrix matrix;

// We can be in one of these 3 states
static final int NONE = 0;
static final int DRAG = 1;
static final int ZOOM = 2;
int mode = NONE;

// Remember some things for zooming
PointF last = new PointF();
PointF start = new PointF();
float minScale = 1f;
float maxScale = 3f;
float[] m;

int viewWidth, viewHeight;
static final int CLICK = 3;
float saveScale = 1f;
protected float origWidth, origHeight;
int oldMeasuredWidth, oldMeasuredHeight;

ScaleGestureDetector mScaleDetector;

Context context;

public TouchImageView(Context context) {
    super(context);
    sharedConstructing(context);
}

public TouchImageView(Context context, AttributeSet attrs) {
    super(context, attrs);
    sharedConstructing(context);
}

GestureDetector mGestureDetector;

private void sharedConstructing(Context context) {
    super.setClickable(true);
    this.context = context;
    mGestureDetector = new GestureDetector(context, this);
    mGestureDetector.setOnDoubleTapListener(this);

    mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
    matrix = new Matrix();
    m = new float[9];
    setImageMatrix(matrix);
    setScaleType(ScaleType.MATRIX);

    setOnTouchListener(new OnTouchListener() {

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            mScaleDetector.onTouchEvent(event);
            mGestureDetector.onTouchEvent(event);

            PointF curr = new PointF(event.getX(), event.getY());

            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    last.set(curr);
                    start.set(last);
                    mode = DRAG;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (mode == DRAG) {
                        float deltaX = curr.x - last.x;
                        float deltaY = curr.y - last.y;
                        float fixTransX = getFixDragTrans(deltaX, viewWidth,
                                origWidth * saveScale);
                        float fixTransY = getFixDragTrans(deltaY, viewHeight,
                                origHeight * saveScale);
                        matrix.postTranslate(fixTransX, fixTransY);
                        fixTrans();
                        last.set(curr.x, curr.y);
                    }
                    break;

                case MotionEvent.ACTION_UP:
                    mode = NONE;
                    int xDiff = (int) Math.abs(curr.x - start.x);
                    int yDiff = (int) Math.abs(curr.y - start.y);
                    if (xDiff < CLICK && yDiff < CLICK)
                        performClick();
                    break;

                case MotionEvent.ACTION_POINTER_UP:
                    mode = NONE;
                    break;
            }

            setImageMatrix(matrix);
            invalidate();
            return true; // indicate event was handled
        }

    });
}

public void setMaxZoom(float x) {
    maxScale = x;
}

@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
    return false;
}

@Override
public boolean onDoubleTap(MotionEvent e) {
    // Double tap is detected
    float origScale = saveScale;
    float mScaleFactor;

    if (saveScale == maxScale) {
        saveScale = minScale;
        mScaleFactor = minScale / origScale;
    } else {
        saveScale = maxScale;
        mScaleFactor = maxScale / origScale;
    }

    matrix.postScale(mScaleFactor, mScaleFactor, viewWidth / 2,
            viewHeight / 2);

    fixTrans();
    return false;
}

@Override
public boolean onDoubleTapEvent(MotionEvent e) {
    return false;
}

@Override
public boolean onDown(MotionEvent e) {
    return false;
}

@Override
public void onShowPress(MotionEvent e) {

}

@Override
public boolean onSingleTapUp(MotionEvent e) {
    return false;
}

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    return false;
}

@Override
public void onLongPress(MotionEvent e) {

}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    return false;
}

private class ScaleListener extends
        ScaleGestureDetector.SimpleOnScaleGestureListener {
    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        mode = ZOOM;
        return true;
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float mScaleFactor = detector.getScaleFactor();
        float origScale = saveScale;
        saveScale *= mScaleFactor;
        if (saveScale > maxScale) {
            saveScale = maxScale;
            mScaleFactor = maxScale / origScale;
        } else if (saveScale < minScale) {
            saveScale = minScale;
            mScaleFactor = minScale / origScale;
        }

        if (origWidth * saveScale <= viewWidth
                || origHeight * saveScale <= viewHeight)
            matrix.postScale(mScaleFactor, mScaleFactor, viewWidth / 2,
                    viewHeight / 2);
        else
            matrix.postScale(mScaleFactor, mScaleFactor,
                    detector.getFocusX(), detector.getFocusY());

        fixTrans();
        return true;
    }
}

void fixTrans() {
    matrix.getValues(m);
    float transX = m[Matrix.MTRANS_X];
    float transY = m[Matrix.MTRANS_Y];

    float fixTransX = getFixTrans(transX, viewWidth, origWidth * saveScale);
    float fixTransY = getFixTrans(transY, viewHeight, origHeight
            * saveScale);

    if (fixTransX != 0 || fixTransY != 0)
        matrix.postTranslate(fixTransX, fixTransY);
}

float getFixTrans(float trans, float viewSize, float contentSize) {
    float minTrans, maxTrans;

    if (contentSize <= viewSize) {
        minTrans = 0;
        maxTrans = viewSize - contentSize;
    } else {
        minTrans = viewSize - contentSize;
        maxTrans = 0;
    }

    if (trans < minTrans)
        return -trans + minTrans;
    if (trans > maxTrans)
        return -trans + maxTrans;
    return 0;
}

float getFixDragTrans(float delta, float viewSize, float contentSize) {
    if (contentSize <= viewSize) {
        return 0;
    }
    return delta;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    viewWidth = MeasureSpec.getSize(widthMeasureSpec);
    viewHeight = MeasureSpec.getSize(heightMeasureSpec);

    //
    // Rescales image on rotation
    //
    if (oldMeasuredHeight == viewWidth && oldMeasuredHeight == viewHeight
            || viewWidth == 0 || viewHeight == 0)
        return;
    oldMeasuredHeight = viewHeight;
    oldMeasuredWidth = viewWidth;

    if (saveScale == 1) {
        // Fit to screen.
        float scale;

        Drawable drawable = getDrawable();
        if (drawable == null || drawable.getIntrinsicWidth() == 0
                || drawable.getIntrinsicHeight() == 0)
            return;
        int bmWidth = drawable.getIntrinsicWidth();
        int bmHeight = drawable.getIntrinsicHeight();

        float scaleX = (float) viewWidth / (float) bmWidth;
        float scaleY = (float) viewHeight / (float) bmHeight;
        scale = Math.min(scaleX, scaleY);
        matrix.setScale(scale, scale);

        // Center the image
        float redundantYSpace = (float) viewHeight
                - (scale * (float) bmHeight);
        float redundantXSpace = (float) viewWidth
                - (scale * (float) bmWidth);
        redundantYSpace /= (float) 2;
        redundantXSpace /= (float) 2;

        matrix.postTranslate(redundantXSpace, redundantYSpace);

        origWidth = viewWidth - 2 * redundantXSpace;
        origHeight = viewHeight - 2 * redundantYSpace;
        setImageMatrix(matrix);
    }
    fixTrans();
}
问题原因
  1. 传递的binding.tvQuestion.text.toString()仅能获取TextView解析后的纯文本,HTML中的图片标签信息完全丢失,无法还原图片内容。
  2. drawMultilineTextToBitmap方法仅支持绘制纯文本,没有解析HTML图片、下载并绘制图片的逻辑。
解决方案

推荐使用WebView加载原始HTML的方案,实现简单且自带缩放功能,无需手动处理图片下载和绘制:

步骤1:修改点击事件,传递原始HTML字符串

binding.tvQuestion.setOnClickListener {
    Intent(requireContext, FullViewImageActivity::class.java).also {
        // 替换为你的原始HTML字符串变量
        it.putExtra("html", originalHtmlString)
        startActivity(it)
    }
}

步骤2:修改FullViewImageActivity布局,替换为WebView

<LinearLayout
    android:id="@+id/mainLay"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    android:orientation="vertical">

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

步骤3:修改FullViewImageActivity代码,加载HTML并配置缩放

class FullViewImageActivity : AppCompatActivity() {
    lateinit var binding: ActivityFullViewImageBinding

    private var html: String? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityFullViewImageBinding.inflate(layoutInflater)
        setContentView(binding.root)
        html = intent.extras!!.getString("html")

        if (!TextUtils.isEmpty(html)) {
            // 配置WebView支持缩放
            binding.webView.settings.apply {
                setSupportZoom(true)
                builtInZoomControls = true
                displayZoomControls = false
                loadWithOverviewMode = true
                useWideViewPort = true
            }
            // 加载HTML并设置白色背景、内边距和字体大小
            val formattedHtml = "<html><body style='background:white; padding:16px; font-size:14pt;'>$html</body></html>"
            binding.webView.loadDataWithBaseURL(null, formattedHtml, "text/html", "UTF-8", null)
        } else {
            Toast.makeText(this, "内容为空", Toast.LENGTH_SHORT).show()
        }
    }
}

如果坚持使用Bitmap绘制方案,需要解析HTML中的图片URL、异步下载图片后绘制到Canvas,可使用Jsoup解析HTML,同时添加网络权限,但实现复杂度较高,不推荐。

内容的提问来源于stack exchange,提问作者Android dev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 10:19:56