Android 使用存储的位置缩放参数在Canvas绘制生成PDF时位置缩放异常
问题背景
我们正在开发一款Android应用,支持用户上传文字和图片,在屏幕任意位置拖拽、缩放后保存为PDF。
数据存储规则
上传图片或文字时,我们会将如下数据存储至Firebase:
- 通过
view.x获取的x轴位置 view.y获取的y轴位置view.scaleX获取的横向缩放系数view.scaleY获取的纵向缩放系数- 若内容为图片则
data字段存储图片URL,若为文字则data字段存储文字字符串
效果对比
设备上传时的效果:
后续我们仅留存上述存储参数,无实际视图对象,需要基于这些参数还原上传时的视图效果生成PDF。生成PDF时我们通过Canvas绘制对应内容,但最终生成的PDF虽包含所有数据,位置和缩放参数却完全不符合预期。
保存为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绘制逻辑错误
- 缩放计算颠倒:
view.scaleX是视图放大比例,你用resource.width / scaleX等于反向缩小了图片,应该乘以scaleX - 裁剪区域错误:src是从原Bitmap取绘制的区域,你填入xLeft/yTop等于从原图对应坐标裁剪,而原图不存在这个偏移,应该从(0,0)取整张图
- 缩放逻辑未生效:你创建了scaledBitmap,但实际绘制时用了原Bitmap,等于缩放代码完全没用
- 目标区域计算错误:dest的右下角应该是xLeft+缩放后宽度、yTop+缩放后高度,不是直接填缩放后的宽高
二、文字绘制逻辑错误
- 只处理了横向缩放
scaleX,完全没用到纵向缩放scaleY,文字纵向比例错误 - 坐标计算错误:你存储的
view.y是视图左上角坐标,而Canvas.drawText的y参数是基线坐标,直接用会导致文字整体偏上 - 单位适配错误:用屏幕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
相关产品推荐
相关产品推荐

