How to create watermark text effect in jetpack compose - android

I would like to create a watermark effect in my app using text as shown in the picture below.
I achieved this by using canvas and bitmap, is there any other reliable way to do this?
Here is my composable function
#Composable
fun WaterMark(
modifier: Modifier = Modifier,
content: (#Composable BoxScope.() -> Unit)? = null,
) {
val watermarkText: String = "some mutable text"
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.textSize = LocalContext.current.dpToPx(24).toFloat()
paint.color = PSCoreColours.psCoreColours.onSurface.hashCode()
paint.textAlign = Paint.Align.LEFT
paint.alpha = (255 * 0.25).toInt()
val baseline: Float = -paint.ascent()
val image: Bitmap = Bitmap.createBitmap(paint.measureText(watermarkText).toInt(),
(baseline + paint.descent()).toInt(),
Bitmap.Config.ARGB_8888)
val canvas = android.graphics.Canvas(image)
canvas.drawText(watermarkText, 0f, baseline, paint)
val rotationMatrix: Matrix = Matrix().apply { postRotate(-45f) }
val rotatedImage: Bitmap = Bitmap.createBitmap(image, 0, 0, image.width, image.height, rotationMatrix, true)
val pattern: ImageBitmap = rotatedImage.asImageBitmap()
Box {
content?.let { it() }
Canvas(
modifier = modifier
) {
val totalWidth = size.width / pattern.width
val totalHeight = size.height / pattern.height
var x = 0f
var y = 0f
for (i in 0..totalHeight.toInt()) {
y = (i * pattern.height).toFloat()
for (j in 0..totalWidth.toInt()) {
x = (j * pattern.width).toFloat()
drawImage(
pattern,
colorFilter = null,
topLeft = Offset(x, y),
)
}
}
}
}
}

You can do custom layouts in compose for this
private const val SPACING = 100
#Composable
fun Watermark(
content: #Composable BoxScope.() -> Unit,
) {
Box {
content()
Layout(
content = {
// Repeating the placeables, 6 should do for now but we should be able to calculate this too
repeat(6) {
Text(
text = watermarkText,
..
)
}
}
) { measurables, constraints ->
// Measuring all the placables
val placeables: List<Placeable> = measurables
.map { measurable -> measurable.measure(constraints) }
layout(constraints.maxWidth, constraints.maxHeight) {
// Calculating the max width of a placable
val maxWidth: Double = placeables.maxOf { it.width }.toDouble()
// Calculating the max width of a tile given the text is rotated
val tileSize: Int = (constraints.maxWidth / atan(maxWidth)).toInt()
placeables
.chunked(2) // Placing 2 columns
.forEachIndexed { index, (first, second) ->
val indexedTileSize: Int = index * tileSize
first.placeRelativeWithLayer(-SPACING, indexedTileSize + SPACING) { rotationZ = -45f }
second.placeRelativeWithLayer(tileSize, indexedTileSize) { rotationZ = -45f }
}
}
}
}
}

Watermark function creates instance of Paint and Bitmap on each recomposition. You should wrap them with remember as in this answer.
However you might, i think, do what you do fully Compose way without Paint and Bitmap either using Modifier.drawWithContent{} and drawText function of DrawScope and using translate or rotate inside DrawScope.
This is a drawText sample to understand how you can create and store TextLayoutResult remember.
And another sample using Modifier.drawWithContent
You can also try using Modifier.drawWithCache to cache TextLayoutResult in layout phase instead of composition phase which is suggested by Google Compose developer works on Text here

Related

How to animate Rect position with Animatable?

I'm building an image cropper. I'm using rectangle to draw dynamic overlay. When overlay is out of image bounds i move it back to image bounds when pointer is up.
What i build
open var overlayRect: Rect =
Rect(offset = Offset.Zero, size = Size(size.width.toFloat(), size.height.toFloat()))
and i get final position using this function to move back to valid bounds
internal fun moveIntoBounds(rectBounds: Rect, rectCurrent: Rect): Rect {
var width = rectCurrent.width
var height = rectCurrent.height
if (width > rectBounds.width) {
width = rectBounds.width
}
if (height > rectBounds.height) {
height = rectBounds.height
}
var rect = Rect(offset = rectCurrent.topLeft, size = Size(width, height))
if (rect.left < rectBounds.left) {
rect = rect.translate(rectBounds.left - rect.left, 0f)
}
if (rect.top < rectBounds.top) {
rect = rect.translate(0f, rectBounds.top - rect.top)
}
if (rect.right > rectBounds.right) {
rect = rect.translate(rectBounds.right - rect.right, 0f)
}
if (rect.bottom > rectBounds.bottom) {
rect = rect.translate(0f, rectBounds.bottom - rect.bottom)
}
return rect
}
And set it on pointer up as
override fun onUp(change: PointerInputChange) {
touchRegion = TouchRegion.None
overlayRect = moveIntoBounds(rectBounds, overlayRect)
// Calculate crop rectangle
cropRect = calculateRectBounds()
rectTemp = overlayRect.copy()
}
How can i animate this rect to valid bounds? Is there way to use Animatable to animate a rect?
I checked official document for animation and suggestion is using transition and
transition.animateRect from one state to another but i don't have states i want to animate to a dynamic target from current dynamic value and this is a non-Composable class called DynamicCropState that extends a class like zoom state here. Need to animate using Animatable or non-Composable apis.
I solved this creating an AnimationVector4D that converts between Float and Rect by
val RectToVector = TwoWayConverter(
convertToVector = { rect: Rect ->
AnimationVector4D(rect.left, rect.top, rect.width, rect.height)
},
convertFromVector = { vector: AnimationVector4D ->
Rect(
offset = Offset(vector.v1, vector.v2),
size = Size(vector.v3, vector.v4)
)
}
)
For demonstration, created a class to animate internally and return current value of Rect
class RectWrapper {
private val animatableRect = Animatable(
Rect(
offset = Offset.Zero,
size = Size(300f, 300f)
),
RectToVector
)
val rect: Rect
get() = animatableRect.value
suspend fun animateRectTo(rect: Rect) {
animatableRect.animateTo(rect)
}
}
And a demonstration to show how to use it
#Composable
private fun AnimateRectWithAnimatable() {
val coroutineScope = rememberCoroutineScope()
val rectWrapper = remember {
RectWrapper()
}
Column(modifier = Modifier.fillMaxSize()) {
Button(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
onClick = {
coroutineScope.launch {
rectWrapper.animateRectTo(
Rect(
topLeft = Offset(200f, 200f),
bottomRight = Offset(800f, 800f)
)
)
}
}
) {
Text("Animate")
}
Canvas(
modifier = Modifier
.fillMaxSize()
) {
drawRect(
color = Color.Red,
topLeft = rectWrapper.rect.topLeft,
size = rectWrapper.rect.size
)
}
}
}
If you wish to animate a Rect from a class you can implement it as above. I generally pass these classes to modifiers as State and observe and trigger changes inside Modifier.composed and return result to any class that uses that modifier.

How to detect what image part was clicked in Android Compose

I am trying to get what part of the image has been clicked and get the Color of that point
I'm able to get the tap coordinates through a Modifier but I don't know how to relate it with the drawable of the Image:
#Composable
fun InteractableImage(
modifier = Modifier
...
) {
val sectionClicked by rememberSaveable { mutableStateOf<Offset>(Offset.Unspecified) }
Image(
modifier = Modifier.fillMaxSize().pointerInput(Unit) {
detectTapGestures (
onTap = { offset -> sectionClicked = offset },
painter = painterResource(id = R.drawable.ic_square),
contentDescription = "square"
)
....
}
With the classic view system, I could access the matrix of the imageView, and with the scale property and the intrinsic drawable dimensions, I could find the part of the image that was clicked. How could I achieve this using Android Compose?
It's a little bit long but working solution except when there are spaces at edges of the Bitmap inside Image because of contentScale param of Image. Need to do another operation for calculating space around but couldn't find it at the moment. And if anyone finds the space just set it to startX, startY and use linear interpolation.
1- Instead of painter get an ImageBitmap
val imageBitmap: ImageBitmap = ImageBitmap.imageResource(
LocalContext.current.resources,
R.drawable.landscape7
)
val bitmapWidth = imageBitmap.width
val bitmapHeight = imageBitmap.height
2- Create variables for touch position on x, y axes and size of the Image that we put bitmap into
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
var imageSize by remember { mutableStateOf(Size.Zero) }
Get Image width from Modifier.onSizeChanged{imageSize = it.toSize()}
3- Get touch position scale it to bitmap position, convert to pixel and return r, g, b values from pixel
.pointerInput(Unit) {
detectTapGestures { offset: Offset ->
// Touch coordinates on image
offsetX = offset.x
offsetY = offset.y
// Scale from Image touch coordinates to range in Bitmap
val scaledX = (bitmapWidth/imageSize.width)*offsetX
val scaledY = (bitmapHeight/imageSize.height)*offsetY
try {
val pixel: Int =
imageBitmap
.asAndroidBitmap()
.getPixel(scaledX.toInt(), scaledY.toInt())
// Don't know if there is a Compose counterpart for this
val red = android.graphics.Color.red(pixel)
val green = android.graphics.Color.green(pixel)
val blue = android.graphics.Color.blue(pixel)
colorInTouchPosition = Color(red,green,blue)
}catch (e:Exception){
println("Exception e: ${e.message}")
}
}
}
4- Instead of scaling and lerping you can just use
val scaledX = (bitmapWidth/endImageX)*offsetX but when your bitmap is not fit into image you need to use linear interpolation after calculating space on left, right, top or bottom of the image, or you should make sure that Image has the same width/height ratio as Bitmap.
i used contentScale = ContentScale.FillBounds for simplicity
// Scale from Image touch coordinates to range in Bitmap
val scaledX = (bitmapWidth/imageSize.width)*offsetX
val scaledY = (bitmapHeight/imageSize.height)*offsetY
Full Implementation
#Composable
private fun TouchOnImageExample() {
val imageBitmap: ImageBitmap = ImageBitmap.imageResource(
LocalContext.current.resources,
R.drawable.landscape6
)
val bitmapWidth = imageBitmap.width
val bitmapHeight = imageBitmap.height
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
var imageSize by remember { mutableStateOf(Size.Zero) }
// These are for debugging
var text by remember { mutableStateOf("") }
var colorInTouchPosition by remember { mutableStateOf(Color.Unspecified) }
val imageModifier = Modifier
.background(Color.LightGray)
.fillMaxWidth()
// This is for displaying different ratio, optional
.aspectRatio(4f / 3)
.pointerInput(Unit) {
detectTapGestures { offset: Offset ->
// Touch coordinates on image
offsetX = offset.x
offsetY = offset.y
// Scale from Image touch coordinates to range in Bitmap
val scaledX = (bitmapWidth/imageSize.width)*offsetX
val scaledY = (bitmapHeight/imageSize.height)*offsetY
// TODO This section needed when Bitmap does not fill Image completely
// However i couldn't find a solution to find spaces correctly
// // Need to calculate spaces at edges of the bitmap inside Image Composable if
// // not exactly filling the bounds of Image
// val startImageX = 0f
// val startImageY = 0f
//
// // End positions, this might be less than Image dimensions if bitmap doesn't fit Image
// val endImageX = imageSize.width - startImageX
// val endImageY = imageSize.height - startImageY
// val scaledX =
// scale(
// start1 = startImageX,
// end1 = endImageX,
// pos = offsetX,
// start2 = 0f,
// end2 = bitmapWidth.toFloat()
// ).coerceAtMost(bitmapWidth.toFloat())
// val scaledY =
// scale(
// start1 = startImageY,
// end1 = endImageY,
// pos = offsetY,
// start2 = 0f,
// end2 = bitmapHeight.toFloat()
// ).coerceAtMost(bitmapHeight.toFloat())
try {
val pixel: Int =
imageBitmap
.asAndroidBitmap()
.getPixel(scaledX.toInt(), scaledY.toInt())
// Don't know if there is a Compose counterpart for this
val red = android.graphics.Color.red(pixel)
val green = android.graphics.Color.green(pixel)
val blue = android.graphics.Color.blue(pixel)
text = "Image Touch: $offsetX, offsetY: $offsetY\n" +
"size: $imageSize\n" +
"bitmap width: ${bitmapWidth}, height: $bitmapHeight\n" +
"scaledX: $scaledX, scaledY: $scaledY\n" +
"red: $red, green: $green, blue: $blue\n"
colorInTouchPosition = Color(red,green,blue)
}catch (e:Exception){
println("Exception e: ${e.message}")
}
}
}
.onSizeChanged { imageSize = it.toSize() }
Image(
bitmap = imageBitmap,
contentDescription = null,
modifier = imageModifier,
contentScale = ContentScale.FillBounds
)
Text(text = text)
Box(
modifier = Modifier
.then(
if (colorInTouchPosition == Color.Unspecified) {
Modifier
} else {
Modifier.background(colorInTouchPosition)
}
)
.size(100.dp)
)
}
/**
* Interpolate position x linearly between start and end
*/
fun lerp(start: Float, end: Float, amount: Float): Float {
return start + amount * (end - start)
}
/**
* Scale x1 from start1..end1 range to start2..end2 range
*/
fun scale(start1: Float, end1: Float, pos: Float, start2: Float, end2: Float) =
lerp(start2, end2, calculateFraction(start1, end1, pos))
/**
* Calculate fraction for value between a range [end] and [start] coerced into 0f-1f range
*/
fun calculateFraction(start: Float, end: Float, pos: Float) =
(if (end - start == 0f) 0f else (pos - start) / (end - start)).coerceIn(0f, 1f)
Result

Wavy box in Jetpack compose

Is there a way to make a box with a wavy top with Canvas?
I would like to know if this effect can be achieved directly with a Canvas, it is not necessary to have a scrolling animation.
It's not quite clear why you're talking about Canvas. To crop a view like this, you can use a custom Shape and apply it to your view with Modifier.clip. Here's a shape you can use:
class WavyShape(
private val period: Dp,
private val amplitude: Dp,
) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density,
) = Outline.Generic(Path().apply {
val wavyPath = Path().apply {
val halfPeriod = with(density) { period.toPx() } / 2
val amplitude = with(density) { amplitude.toPx() }
moveTo(x = -halfPeriod / 2, y = amplitude)
repeat(ceil(size.width / halfPeriod + 1).toInt()) { i ->
relativeQuadraticBezierTo(
dx1 = halfPeriod / 2,
dy1 = 2 * amplitude * (if (i % 2 == 0) 1 else -1),
dx2 = halfPeriod,
dy2 = 0f,
)
}
lineTo(size.width, size.height)
lineTo(0f, size.height)
}
val boundsPath = Path().apply {
addRect(Rect(offset = Offset.Zero, size = size))
}
op(wavyPath, boundsPath, PathOperation.Intersect)
})
}
If you really need to use this inside Canvas for some reason, you can pass the same Path that I create inside WavyShape to DrawScope.clipPath, so that the contents of the clipPath block will be clipped.
Apply custom shape to your Image or any other view:
Image(
painter = painterResource(id = R.drawable.my_image_1),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.clip(WavyShape(period = 100.dp, amplitude = 50.dp))
)
Result:

How to draw rounded corner polygons in Jetpack Compose Canvas?

I'm trying to create a rounded triangle using Canvas in Jetpack Compose.
I try this code for drawing triangle:
#Composable
fun RoundedTriangle() {
Canvas(modifier = Modifier.size(500.dp)) {
val trianglePath = Path().apply {
val height = size.height
val width = size.width
moveTo(width / 2.0f, 0f)
lineTo(width, height)
lineTo(0f, height)
}
drawPath(trianglePath, color = Color.Blue)
}
}
But I don't know how to round the triangle corners. I also tried to use arcTo, but I was unable to get a suitable result.
How can I draw something like the figure below?
For Stroke you can specify rounding like this:
drawPath(
...
style = Stroke(
width = 2.dp.toPx(),
pathEffect = PathEffect.cornerPathEffect(4.dp.toPx())
)
)
Yet Fill seems lack of support rounding. I've created a feature request, please star it.
But Canvas has drawOutline function, which accepts both Outline, which can wrap a Path, and Paint, for which you can specify pathEffect:
Canvas(modifier = Modifier.fillMaxWidth().aspectRatio(1f)) {
val rect = Rect(Offset.Zero, size)
val trianglePath = Path().apply {
moveTo(rect.topCenter)
lineTo(rect.bottomRight)
lineTo(rect.bottomLeft)
close()
}
drawIntoCanvas { canvas ->
canvas.drawOutline(
outline = Outline.Generic(trianglePath),
paint = Paint().apply {
color = Color.Black
pathEffect = PathEffect.cornerPathEffect(rect.maxDimension / 3)
}
)
}
}
Path helpers:
fun Path.moveTo(offset: Offset) = moveTo(offset.x, offset.y)
fun Path.lineTo(offset: Offset) = lineTo(offset.x, offset.y)
Result:
Based on #philip-dukhov answer, if anyone is interested in appliying this to a square
#Composable
fun SquirclePath(
modifier: Modifier,
smoothingFactor: Int = 60,
color: Color,
strokeWidth: Float,
) {
Canvas(
modifier = modifier
) {
val rect = Rect(Offset.Zero, size)
val percent = smoothingFactor.percentOf(rect.minDimension)
val squirclePath = Path().apply {
with(rect) {
lineTo(topRight)
lineTo(bottomRight)
lineTo(bottomLeft)
lineTo(topLeft)
// this is where the path is finally linked together
close()
}
}
drawIntoCanvas { canvas ->
canvas.drawOutline(
outline = Outline.Generic(squirclePath),
paint = Paint().apply {
this.color = color
this.style = PaintingStyle.Fill
this.strokeWidth = strokeWidth
pathEffect = PathEffect.cornerPathEffect(percent)
}
)
}
}
}
fun Int.percentOf(target:Float) = (this.toFloat() / 100) * target

Android Compose - How to tile/repeat a bitmap/vector?

What is the Android Compose approach to tile an image to fill my background with a small pattern?
A naive approach for Bitmaps without rotation could be like this:
#Composable
fun TileImage() {
val pattern = ImageBitmap.imageResource(R.drawable.pattern_bitmap)
Canvas(modifier = Modifier.fillMaxSize()) {
// rotate(degrees = -15f) { // The rotation does not produce the desired effect
val totalWidth = size.width / pattern.width
val totalHeight = size.height / pattern.height
var x = 0f
var y = 0f
for (i in 0..totalHeight.toInt()) {
y = (i * pattern.height).toFloat()
for (j in 0..totalWidth.toInt()) {
x = (j * pattern.width).toFloat()
drawImage(
pattern,
colorFilter = giftColorFilter,
topLeft = Offset(x, y)
)
}
}
// }
}
}
In Android XML you can easily create XML to repeat a bitmap
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="#drawable/pattern_bitmap"
android:tileMode="repeat" />
Or if you need to tile a vector you can use a custom Drawable class to achieve your goal
TileDrawable(AppCompatResources.getDrawable(context, R.drawable.pattern_vector), Shader.TileMode.REPEAT)
class TileDrawable(drawable: Drawable, tileMode: Shader.TileMode, private val angle: Float? = null) : Drawable() {
private val paint: Paint = Paint().apply {
shader = BitmapShader(getBitmap(drawable), tileMode, tileMode)
}
override fun draw(canvas: Canvas) {
angle?.let {
canvas.rotate(it)
}
canvas.drawPaint(paint)
}
override fun setAlpha(alpha: Int) {
paint.alpha = alpha
}
override fun getOpacity() = PixelFormat.TRANSLUCENT
override fun setColorFilter(colorFilter: ColorFilter?) {
paint.colorFilter = colorFilter
}
private fun getBitmap(drawable: Drawable): Bitmap {
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
val bmp = Bitmap.createBitmap(
drawable.intrinsicWidth, drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val c = Canvas(bmp)
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
drawable.draw(c)
return bmp
}
}
If you want to use native canvas you can do something like this in jetpack compose.
Canvas(
modifier = Modifier
.fillMaxSize()
) {
val paint = Paint().asFrameworkPaint().apply {
isAntiAlias = true
shader = ImageShader(pattern, TileMode.Repeated, TileMode.Repeated)
}
drawIntoCanvas {
it.nativeCanvas.drawPaint(paint)
}
paint.reset()
}
And If you want to limit your repetition to a certain height and width you can use the clip modifier in canvas like below otherwise it will fill the entire screen.
Canvas(
modifier = Modifier
.width(300.dp)
.height(200.dp)
.clip(RectangleShape)
) {
----
}
Based on Rafiul's answer, I was able to come up with something a bit more succinct. Here's hoping Compose comes up with something built-in to make this simpler in the future.
val image = ImageBitmap.imageResource(R.drawable.my_image)
val brush = remember(image) { ShaderBrush(ImageShader(image, TileMode.Repeated, TileMode.Repeated)) }
Box(Modifier
.fillMaxSize()
.background(brush)) {
}

Categories

Resources