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

Android 使用存储的位置缩放参数在Canvas绘制生成PDF时位置缩放异常

问题背景

我们正在开发一款Android应用,支持用户上传文字和图片,在屏幕任意位置拖拽、缩放后保存为PDF。

数据存储规则

上传图片或文字时,我们会将如下数据存储至Firebase:

  • 通过view.x获取的x轴位置
  • view.y获取的y轴位置
  • view.scaleX获取的横向缩放系数
  • view.scaleY获取的纵向缩放系数
  • 若内容为图片则data字段存储图片URL,若为文字则data字段存储文字字符串

效果对比

设备上传时的效果:
Uploaded on the device

后续我们仅留存上述存储参数,无实际视图对象,需要基于这些参数还原上传时的视图效果生成PDF。生成PDF时我们通过Canvas绘制对应内容,但最终生成的PDF虽包含所有数据,位置和缩放参数却完全不符合预期。

保存为PDF后的效果:
Saved pdf

现有代码

Canvas绘制Bitmap代码

private suspend fun drawBitmapOnCanvas(
        canvas: Canvas,
        uriStringPath: String,
        boardSketch: BoardSketch? = null
    ) {
        if (uriStringPath.isEmpty()) {
            return
        } else {
            return suspendCoroutine {
                var xLeft = 0f
                var yTop = 0f
                var scaleX = 1F
                var scaleY = 1F

                boardSketch?.matrix?.let { matrix ->
                    scaleX = matrix[2]
                    scaleY = matrix[3]
                }

                Glide.with(this)
                    .asBitmap()
                    .load(uriStringPath)
                    .into(object : CustomTarget<Bitmap>() {
                        override fun onResourceReady(
                            resource: Bitmap,
                            transition: Transition<in Bitmap>?
                        ) {

                            val paint = Paint()
                            paint.isAntiAlias = true
                            paint.isFilterBitmap = true
                            paint.isDither = true

                            boardSketch?.run {
                                matrix?.let { floats ->
                                    xLeft = floats[0]
                                    yTop = floats[1]
                                }
                            }

                            val scaledWidth = resource.width / scaleX
                            val scaledHeight = resource.height / scaleY

                            val scaledBitmap = Bitmap.createScaledBitmap(resource, scaledWidth.toInt(), scaledHeight.toInt(), true)

                            val src = Rect(xLeft.toInt(), yTop.toInt(), scaledBitmap.width, scaledBitmap.height)
                            val dest = Rect(xLeft.toInt(), yTop.toInt(), scaledBitmap.width, scaledBitmap.height)

                            canvas.drawBitmap(resource, src, dest, paint)
                            return it.resume(Unit)
                        }

                        override fun onLoadCleared(placeholder: Drawable?) {
                            // 该方法在生命周期调用等场景下imageView被清理时触发
                            // 若你在imageView以外的地方也引用了该bitmap,可在此处清理,此时已无法持有该bitmap
                        }
                    })
            }
        }
    }

Canvas绘制文字代码

private fun drawTextOnCanvas(sketch: BoardSketch, extraCanvas: Canvas, paint: Paint) {
     
        var xPosition = 16F
        var yPosition = 16F
        var scaleX = 1.0F
        var scaleY = 1.0F

        sketch.matrix?.let {
            xPosition = it[0]
            yPosition = it[1]
            scaleX = it[2]
            scaleY = it[3]
        }

        paint.textScaleX = scaleX
        val textSize = sketch.size ?: AppConstants.DEFAULT_TEXT_SIZE_FOR_ANDROID
        val paintScaledTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, textSize, resources.displayMetrics)
        paint.textSize = paintScaledTextSize
        extraCanvas.drawText(sketch.data, xPosition, yPosition, paint)
    }

问题描述

最终保存的PDF既不能正确展示图片和文字的x、y轴位置,也无法正确应用缩放参数,可能是调用方法错误或计算逻辑有误,需要解决方案。


解决方案

你的代码存在以下几个核心错误,对应修复即可:

一、Bitmap绘制逻辑错误

  1. 缩放计算颠倒:view.scaleX是视图放大比例,你用resource.width / scaleX等于反向缩小了图片,应该乘以scaleX
  2. 裁剪区域错误:src是从原Bitmap取绘制的区域,你填入xLeft/yTop等于从原图对应坐标裁剪,而原图不存在这个偏移,应该从(0,0)取整张图
  3. 缩放逻辑未生效:你创建了scaledBitmap,但实际绘制时用了原Bitmap,等于缩放代码完全没用
  4. 目标区域计算错误:dest的右下角应该是xLeft+缩放后宽度、yTop+缩放后高度,不是直接填缩放后的宽高

二、文字绘制逻辑错误

  1. 只处理了横向缩放scaleX,完全没用到纵向缩放scaleY,文字纵向比例错误
  2. 坐标计算错误:你存储的view.y是视图左上角坐标,而Canvas.drawText的y参数是基线坐标,直接用会导致文字整体偏上
  3. 单位适配错误:用屏幕displayMetrics转换SP是针对屏幕显示的,PDF Canvas是独立单位,不需要做屏幕适配

修正后的代码

修正后的Bitmap绘制代码

private suspend fun drawBitmapOnCanvas(
    canvas: Canvas,
    uriStringPath: String,
    boardSketch: BoardSketch? = null
) {
    if (uriStringPath.isEmpty()) return
    return suspendCoroutine { continuation ->
        var xLeft = 0f
        var yTop = 0f
        var scaleX = 1F
        var scaleY = 1F

        boardSketch?.matrix?.let { matrix ->
            xLeft = matrix[0]
            yTop = matrix[1]
            scaleX = matrix[2]
            scaleY = matrix[3]
        }

        Glide.with(this)
            .asBitmap()
            .load(uriStringPath)
            .into(object : CustomTarget<Bitmap>() {
                override fun onResourceReady(
                    resource: Bitmap,
                    transition: Transition<in Bitmap>?
                ) {
                    val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
                        isFilterBitmap = true
                        isDither = true
                    }
                    // 正确计算缩放后的宽高
                    val scaledWidth = (resource.width * scaleX).toInt()
                    val scaledHeight = (resource.height * scaleY).toInt()
                    // src取整张原图
                    val src = Rect(0, 0, resource.width, resource.height)
                    // dest对应PDF上的绘制区域
                    val dest = Rect(xLeft.toInt(), yTop.toInt(), (xLeft + scaledWidth).toInt(), (yTop + scaledHeight).toInt())
                    canvas.drawBitmap(resource, src, dest, paint)
                    continuation.resume(Unit)
                }

                override fun onLoadCleared(placeholder: Drawable?) {}
            })
    }
}

修正后的文字绘制代码

private fun drawTextOnCanvas(sketch: BoardSketch, extraCanvas: Canvas, paint: Paint) {
    var xPosition = 16F
    var yPosition = 16F
    var scaleX = 1.0F
    var scaleY = 1.0F

    sketch.matrix?.let {
        xPosition = it[0]
        yPosition = it[1]
        scaleX = it[2]
        scaleY = it[3]
    }
    // 文字大小适配纵向缩放
    val baseTextSize = sketch.size ?: AppConstants.DEFAULT_TEXT_SIZE_FOR_ANDROID
    paint.textSize = baseTextSize * scaleY
    // 横向缩放
    paint.textScaleX = scaleX
    // 修正基线偏移,让文字y坐标和存储的view.y对齐
    val fontMetrics = paint.fontMetrics
    val baselineOffset = -fontMetrics.top
    extraCanvas.drawText(sketch.data, xPosition, yPosition + baselineOffset, paint)
}

额外注意

如果生成PDF的Canvas尺寸和编辑时的视图尺寸不一致,需要先按比例统一转换所有x/y/scale参数后再绘制,同时保证两者的坐标系原点都在左上角。

内容的提问来源于stack exchange,提问作者Sagar Patel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.29 14:36:03