Canvas Drawing Small Text - android

I am trying to draw text on bitmap but text size differs in different pictures. Text size keeps is too small on some bitmaps and large on some. I have noticed that it is small on big resolution bitmaps.
This is how I'm trying to draw. Please tell what I'm doing wrong
val originalBitmap = MediaStore.Images.Media.getBitmap(
context.contentResolver,
uri
)
val bitmap = convertToMutable(
originalBitmap
)
val ratio = originalBitmap.width/bitmap.width
val scale = context.resources.displayMetrics.density
val canvas = Canvas(bitmap)
val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
textPaint.color = Color.WHITE
textPaint.textSize = 7f * scale
textPaint.style = Paint.Style.FILL
val locBound = Rect()
textPaint.getTextBounds(location,0,location.length,locBound)
val txtWidth = (canvas.width - 15)
val txtHeight = textPaint.measureText("yY")
val txtTopX = ((bitmap.width - txtWidth) - 15).toFloat()
val txtTopY = ((bitmap.height - txtHeight) - 20).toFloat()
canvas.drawMultilineText(
location,
textPaint,
txtWidth,
txtTopX,
txtTopY,
0,
location.length,
Layout.Alignment.ALIGN_OPPOSITE
)`
fun Canvas.drawMultilineText(
text: CharSequence,
textPaint: TextPaint,
width: Int,
x: Float,
y: Float,
start: Int = 0,
end: Int = text.length,
alignment: Layout.Alignment = Layout.Alignment.ALIGN_NORMAL,
spacingMult: Float = 1f,
spacingAdd: Float = 0f,
includePad: Boolean = true,
ellipsizedWidth: Int = width,
ellipsize: TextUtils.TruncateAt? = null
) {
val cacheKey = "$text-$start-$end-$textPaint-$width-$alignment-" +
"$spacingMult-$spacingAdd-$includePad-$ellipsizedWidth-$ellipsize"
// The public constructor was deprecated in API level 28,
// but the builder is only available from API level 23 onwards
val staticLayout =
StaticLayoutCache[cacheKey] ?: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
StaticLayout.Builder.obtain(text, start, end, textPaint, width)
.setAlignment(alignment)
.setLineSpacing(spacingAdd, spacingMult)
.setIncludePad(includePad)
.setEllipsizedWidth(ellipsizedWidth)
.setEllipsize(ellipsize)
.build()
} else {
StaticLayout(
text, start, end, textPaint, width, alignment,
spacingMult, spacingAdd, includePad, ellipsize, ellipsizedWidth
)
.apply { StaticLayoutCache[cacheKey] = this }
}
staticLayout.draw(this, x, y)
}
Here is the result I'm getting
First Image Second Image Third Image

You can set font size based on your image size. for example your font size can be 0.03 of image size. So in this way your font size on image can be change base on your image size. so you need to do this as bellow:
var imageSize = max(originalBitmap.width, originalBitmap.height)
var fontScale = 0.03
var textSize = imageSize * fontScale
textPaint.textSize = textSize
You must note that image can be landscape or portrait. so we get width or height that is greater as imageSize. You can change fontScale according to your needs.
Also, I think there is no need to use scale variable in textSize computing. because it is a display metric and can be different in various devices, so must not be affected in bitmap text size, and text size must only define based on bitmap image size.

Related

The border or shadow cannot be set after BottomNavigationView sets the top bulge

I set the bulge for BottomNavigationView through the set to edge method of materialShapeDrawable, because the color of the page is similar to that of BottomNavigationView, so I need to set a border or shadow on top of the control, but I can't set the border or shadow on top of the control normally.
In addition, bottomNavigationView.invalidateOutline() seems to have some effect, but it seems to be only valid on Android12, and the lower version of Android cannot be used.
val materialShapeDrawable = bottomNavigationView.background as MaterialShapeDrawable materialShapeDrawable.shapeAppearanceModel = materialShapeDrawable.shapeAppearanceModel
.toBuilder()
.setTopEdge(CutoutCircleEdgeTreatment(resources, 70.toFloat(), 10.toFloat()))
.build()
val backgroundDrawable =
MaterialShapeDrawable(materialShapeDrawable.shapeAppearanceModel).apply {
setTint(Color.WHITE)
paintStyle = Paint.Style.FILL
shadowCompatibilityMode = MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS
initializeElevationOverlay(this#MainActivity)
setShadowColor(Color.RED)
elevation = 100F
}
(bottomNavigationView.parent as? ViewGroup)?.clipChildren = false
bottomNavigationView.background = backgroundDrawable
bottomNavigationView.invalidateOutline()
class CutoutCircleEdgeTreatment(
res: Resources,
circleDiameterDp: Float,
circleLeftRightOffsetDp: Float
) : EdgeTreatment() {
private val fabDiameter: Float
private val offset: Float
init {
fabDiameter = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
circleDiameterDp,
res.displayMetrics
)
offset = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
circleLeftRightOffsetDp,
res.displayMetrics
)
}
override fun getEdgePath(
length: Float,
center: Float,
interpolation: Float,
shapePath: ShapePath
) {
//凸起半径
val r = length / 10 // == center
val halfLine = length / 2 - r
//值越大转角处就越顺滑,但是必须小于等于r,这个大小可以多试试
val offset = 30f
//第一部分,直线,画到凹陷开始的地方
shapePath.lineTo(halfLine - offset, 0f)
//凸起开始的地方的转角,不处理的话看起来很尖锐
shapePath.quadToPoint(halfLine, 0f, halfLine + offset, -offset)
//凸起部分
shapePath.quadToPoint(center, -r, halfLine + 2 * r - offset, -offset)
//凸起结束的地方的转角
shapePath.quadToPoint(halfLine + 2 * r, 0f, halfLine + 2 * r + offset, 0f)
// 最后一部分的直线,画到底
shapePath.lineTo(length, 0f)
}
}
enter image description here
enter image description here

Kotlin - thermal printer Arabic text never printed

Am using DantSu library to print receipts but Arabic text is never printed
here is my code
val printer = EscPosPrinter(
deviceConnection,
printerData.printerDpi,
printerData.printerWidthMM,
printerData.printerNbrCharactersPerLine,
EscPosCharsetEncoding("windows-1252", 16)
)
According to this comment on the issue on the library .. we found a solution but not totally correct
private fun getBitmap(text: String): Bitmap? {
val paint = Paint()
paint.setTextSize(28f)
val bounds = Rect()
paint.getTextBounds(text, 0, text.length, bounds)
val width: Int = bounds.width()
val height: Int = bounds.height()
val bitmap = Bitmap.createBitmap(width * 2, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawColor(Color.WHITE)
canvas.drawText(text, 20f, 20f, paint)
return bitmap
}

How to keep the same color alpha when 2 lines are being overlapped with Canvas

I'm using Canvas with the method drawLines(...) and I have 2 lines crossing/overlapping each other, these lines have a color alpha of 0.3f.
The issue is that when the cross each other this alpha is accumulated, converting their crosspoint with a color alpha of 0.6f.
Is there any way to keep the original alpha even if they cross each other?
Step 1. create Path from start point, end point and stroke width:
fun linePath(start: Offset, end: Offset, strokeWidth: Float) =
Path().apply {
// calculations from https://stackoverflow.com/a/7854359/3585796
val size = end - start
val perpendicular = Offset(size.y, -size.x)
val length = hypot(size.x, size.y)
val normalized = Offset(perpendicular.x / length, perpendicular.y / length)
val points = listOf(
end + normalized * strokeWidth / 2f,
start + normalized * strokeWidth / 2f,
start - normalized * strokeWidth / 2f,
end - normalized * strokeWidth / 2f,
)
moveTo(points.last())
points.forEach(::lineTo)
}
fun Path.moveTo(offset: Offset) =
moveTo(offset.x, offset.y)
fun Path.lineTo(offset: Offset) =
lineTo(offset.x, offset.y)
Step 2. Using clipPath draw needed parts of the lines:
Canvas(Modifier.fillMaxSize()) {
val rect = Rect(Offset.Zero, size)
val firstLinePath = linePath(
start = rect.topLeft,
end = rect.bottomRight,
strokeWidth = 20f,
)
val firstLineColor = Color.Red
val secondLinePath = linePath(
start = rect.topRight,
end = rect.bottomLeft,
strokeWidth = 20f,
)
val secondLineColor = Color.Green
val sharedAlpha = 0.5f
// draw first line without intersection part
clipPath(secondLinePath, clipOp = ClipOp.Difference) {
drawPath(firstLinePath, color = firstLineColor.copy(sharedAlpha))
}
// draw second line without intersection part
clipPath(firstLinePath, clipOp = ClipOp.Difference) {
drawPath(secondLinePath, color = secondLineColor.copy(sharedAlpha))
}
// draw intersection part with mixed color
clipPath(secondLinePath) {
val blendedColor = Color(
ColorUtils.blendARGB(
firstLineColor.toArgb(),
secondLineColor.toArgb(),
0.5f
)
)
drawPath(firstLinePath, color = blendedColor.copy(sharedAlpha))
}
}
Result:
No. This will not be possible since we would have to modify the value of alpha of a specific "portion" of the Composable, which is not something achievable easily. You need to change your approach.

Jetpack Compose Applying PorterDuffMode to Image

Based on the images and PorterDuffModes in this page
I downloaded images, initially even though they are png they had light and dark gray rectangles which were not transparent and removed them.
And checked out using this sample code, replacing drawables with the ones in original code with the ones below and i get result
As it seem it works as it should with Android View, but when i use Jetpack Canvas as
androidx.compose.foundation.Canvas(modifier = Modifier.size(500.dp),
onDraw = {
drawImage(imageBitmapDst)
drawImage(imageBitmapSrc, blendMode = BlendMode.SrcIn)
})
BlendMode.SrcIn draws blue rectangle over black rectangle, other modes do not return correct results either. BlendMode.SrcOut returns black screen.
And using 2 Images stacked on top of each other with Box
val imageBitmapSrc: ImageBitmap = imageResource(id = R.drawable.c_src)
val imageBitmapDst: ImageBitmap = imageResource(id = R.drawable.c_dst)
Box {
Image(bitmap = imageBitmapSrc)
Image(
bitmap = imageBitmapDst,
colorFilter = ColorFilter(color = Color.Unspecified, blendMode = BlendMode.SrcOut)
)
}
Only blue src rectangle is visible.
Also tried with Painter, and couldn't able to make it work either
val imageBitmapSrc: ImageBitmap = imageResource(id = R.drawable.c_src)
val imageBitmapDst: ImageBitmap = imageResource(id = R.drawable.c_dst)
val blendPainter = remember {
object : Painter() {
override val intrinsicSize: Size
get() = Size(imageBitmapSrc.width.toFloat(), imageBitmapSrc.height.toFloat())
override fun DrawScope.onDraw() {
drawImage(imageBitmapDst, blendMode = BlendMode.SrcOut)
drawImage(imageBitmapSrc)
}
}
}
Image(blendPainter)
How should Blend or PorterDuff mode be used with Jetpack Compose?
Easiest way to solve issue is to add
.graphicsLayer(alpha = 0.99f) to Modifier to make sure an offscreen buffer
#Composable
fun DrawWithBlendMode() {
val imageBitmapSrc = ImageBitmap.imageResource(
LocalContext.current.resources,
R.drawable.composite_src
)
val imageBitmapDst = ImageBitmap.imageResource(
LocalContext.current.resources,
R.drawable.composite_dst
)
Canvas(
modifier = Modifier
.fillMaxSize()
// Provide a slight opacity to for compositing into an
// offscreen buffer to ensure blend modes are applied to empty pixel information
// By default any alpha != 1.0f will use a compositing layer by default
.graphicsLayer(alpha = 0.99f)
) {
val dimension = (size.height.coerceAtMost(size.width) / 2f).toInt()
drawImage(
image = imageBitmapDst,
dstSize = IntSize(dimension, dimension)
)
drawImage(
image = imageBitmapSrc,
dstSize = IntSize(dimension, dimension),
blendMode = BlendMode.SrcOut
)
}
}
Result
Or adding a layer in Canvas does the trick
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawImage(
image = dstImage,
srcSize = IntSize(canvasWidth / 2, canvasHeight / 2),
dstSize = IntSize(canvasWidth, canvasHeight),
)
// Source
drawImage(
image = srcImage,
srcSize = IntSize(canvasWidth / 2, canvasHeight / 2),
dstSize = IntSize(canvasWidth, canvasHeight),
blendMode = blendMode
)
restoreToCount(checkPoint)
}
I created some tutorials for applying blend modes here
I was really frustrated for a whole week with similar problem, however your question helped me find the solution how to make it work.
EDIT1
I'm using compose 1.0.0
In my case I'm using something like double buffering instead of drawing directly on canva - just as a workaround.
Canvas(modifier = Modifier.fillMaxWidth().fillMaxHeight()) {
// First I create bitmap with real canva size
val bitmap = ImageBitmap(size.width.toInt(), size.height.toInt())
// here I'm creating canvas of my bitmap
Canvas(bitmap).apply {
// here I'm driving on canvas
}
// here I'm drawing my buffered image
drawImage(bitmap)
}
Inside Canvas(bitmap) I'm using drawPath, drawText, etc with paint:
val colorPaint = Paint().apply {
color = Color.Red
blendMode = BlendMode.SrcAtop
}
And in this way BlendMode works correctly - I've tried many of modes and everything worked as expected.
I don't know why this isn't working directly on canvas of Composable, but my workaround works fine for me.
EDIT2
After investigating Image's Painter's source code i saw that Android team also use alpha trick either to decide to create a layer or not
In Painter
private fun configureAlpha(alpha: Float) {
if (this.alpha != alpha) {
val consumed = applyAlpha(alpha)
if (!consumed) {
if (alpha == DefaultAlpha) {
// Only update the paint parameter if we had it allocated before
layerPaint?.alpha = alpha
useLayer = false
} else {
obtainPaint().alpha = alpha
useLayer = true
}
}
this.alpha = alpha
}
}
And applies here
fun DrawScope.draw(
size: Size,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null
) {
configureAlpha(alpha)
configureColorFilter(colorFilter)
configureLayoutDirection(layoutDirection)
// b/156512437 to expose saveLayer on DrawScope
inset(
left = 0.0f,
top = 0.0f,
right = this.size.width - size.width,
bottom = this.size.height - size.height
) {
if (alpha > 0.0f && size.width > 0 && size.height > 0) {
if (useLayer) {
val layerRect = Rect(Offset.Zero, Size(size.width, size.height))
// TODO (b/154550724) njawad replace with RenderNode/Layer API usage
drawIntoCanvas { canvas ->
canvas.withSaveLayer(layerRect, obtainPaint()) {
onDraw()
}
}
} else {
onDraw()
}
}
}
}
}

How to draw slices inside a circle without radio as center

I need to draw a circle and fill it according to a percentage value as image shows.
]2
I have tried to draw it using canvas:
#SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
rectF = RectF(0f + margin, 0f + margin, width.toFloat() - margin, height.toFloat() - margin)
val scaledValues = scale()
var sliceStartPoint = 0F
for (i in scaledValues.indices) {
slicePaint.color = ContextCompat.getColor(context, sliceColors[i])
canvas!!.drawArc(rectF!!, sliceStartPoint, scaledValues[i], true, slicePaint)
canvas.drawArc(rectF!!, sliceStartPoint, scaledValues[i], true, centerPaint)
sliceStartPoint += scaledValues[i]
}
}
The problem is that I got all lines centered to radio (as a Pie) instead of slices.
I tried to set usecenter:false but it was not successful result.
Any idea how to draw an arc without set the center point as vertice?
I think you just need rotate the canvas & a little math.
private val slicePaint = Paint()
private val centerPaint = Paint().apply {
color = 0x33333333
}
private val sliceColors = arrayListOf<Int>(Color.GRAY, Color.RED, Color.BLACK)
private var rectF: RectF? = null
private val margin = 0f
private var rotDeg = -120f
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
rectF = RectF(0f + margin, 0f + margin, width.toFloat() - margin, height.toFloat() - margin)
canvas.save()
canvas.rotate(rotDeg, canvas.width /2.0f, canvas.height/2.0f)
// val scaledValues = scale()
// var sliceStartPoint = 0F
for (i in percent.indices) {
slicePaint.color = sliceColors[i] // ContextCompat.getColor(context, sliceColors[i])
var d = percent2degree(percent[i])
canvas!!.drawArc(rectF!!, d, -2 * d, false, slicePaint)
// canvas.drawArc(rectF!!, sliceStartPoint, scaledValues[i], true, centerPaint)
// sliceStartPoint += scaledValues[i]
}
canvas.restore()
}
private fun percent2degree(p:Float): Float {
return (acos((0.5 - p)/ 0.5) / Math.PI * 180).toFloat()
}
var percent = arrayOf(1.0f, 0.9f, 0.55f) // 0.5 + 0.3 + 0.2 = 100%

Categories

Resources