Wavy box in Jetpack compose - android

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:

Related

Jetpack Compose: How to create a rating bar?

I'm trying to implement a rating bar. I refer to https://gist.github.com/vitorprado/0ae4ad60c296aefafba4a157bb165e60 but I don't understand anything from this code. It works but when I use this code the stars don't have rounded corners. I want to implement something like the following :
I made very basic sample for this, it would give the basic idea for creating rating bar with sample border and filled png files.
#Composable
private fun RatingBar(
modifier: Modifier = Modifier,
rating: Float,
spaceBetween: Dp = 0.dp
) {
val image = ImageBitmap.imageResource(id = R.drawable.star)
val imageFull = ImageBitmap.imageResource(id = R.drawable.star_full)
val totalCount = 5
val height = LocalDensity.current.run { image.height.toDp() }
val width = LocalDensity.current.run { image.width.toDp() }
val space = LocalDensity.current.run { spaceBetween.toPx() }
val totalWidth = width * totalCount + spaceBetween * (totalCount - 1)
Box(
modifier
.width(totalWidth)
.height(height)
.drawBehind {
drawRating(rating, image, imageFull, space)
})
}
private fun DrawScope.drawRating(
rating: Float,
image: ImageBitmap,
imageFull: ImageBitmap,
space: Float
) {
val totalCount = 5
val imageWidth = image.width.toFloat()
val imageHeight = size.height
val reminder = rating - rating.toInt()
val ratingInt = (rating - reminder).toInt()
for (i in 0 until totalCount) {
val start = imageWidth * i + space * i
drawImage(
image = image,
topLeft = Offset(start, 0f)
)
}
drawWithLayer {
for (i in 0 until totalCount) {
val start = imageWidth * i + space * i
// Destination
drawImage(
image = imageFull,
topLeft = Offset(start, 0f)
)
}
val end = imageWidth * totalCount + space * (totalCount - 1)
val start = rating * imageWidth + ratingInt * space
val size = end - start
// Source
drawRect(
Color.Transparent,
topLeft = Offset(start, 0f),
size = Size(size, height = imageHeight),
blendMode = BlendMode.SrcIn
)
}
}
private fun DrawScope.drawWithLayer(block: DrawScope.() -> Unit) {
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
block()
restoreToCount(checkPoint)
}
}
Usage
Column {
RatingBar(rating = 3.7f, spaceBetween = 3.dp)
RatingBar(rating = 2.5f, spaceBetween = 2.dp)
RatingBar(rating = 4.5f, spaceBetween = 2.dp)
RatingBar(rating = 1.3f, spaceBetween = 4.dp)
}
Result
Also created a library that uses gestures, other png files and vectors as rating items is available here.
RatingBar(
rating = rating,
space = 2.dp,
imageBackground = imageBackground,
imageForeground = imageForeground,
animationEnabled = false,
gestureEnabled = true,
itemSize = 60.dp
) {
rating = it
}
You can pass the custom drawable as icon. check this code.
Replace your RatingStar() function as it is using canvas to draw star, instead pass the custom drawable.
#Composable
private fun starShow(){
val icon = if (isSelected)
//your selected drawable
else
//your unselected drawable
Icon(
painter = painterResource(id = icon),
contentDescription = null,
tint = MyColor.starColor)
}

How to create watermark text effect in jetpack compose

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

Jetpack compose Canvas blendmode not working as expected

I'm attempting to apply blend mode to two shapes within Jetpack compose's canvas. Based on this blog I know roughly what the expected output should look like though I am not getting similar results.
For example, with the following simple Box + Canvas with two shapes, with the blend mode SrcIn
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.size(290.dp)
) {
val sizeInPx = with(LocalDensity.current) { 150.dp.toPx() }
Canvas(
modifier = Modifier.fillMaxSize()
) {
drawCircle(
color = Color.Red,
radius = sizeInPx,
)
drawRect(
color = Color.Blue,
size = Size(sizeInPx, sizeInPx),
blendMode = BlendMode.SrcIn
)
}
}
I would expect a red circle, and a blue square clipped to the shape of the red circle. Yet the output UI is as if no blend mode has been added at all
What am I doing wrong?
Changing alpha less then 1f creates a layer as buffer that's why it works. Other way of achieving this is to use layer directly if you don't want to change alpha. You can see my answere about it here
Canvas(modifier = canvasModifier) {
val canvasWidth = size.width.roundToInt()
val canvasHeight = size.height.roundToInt()
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
drawCircle(
color = Color.Red,
radius = sizeInPx,
)
drawRect(
color = Color.Blue,
size = Size(sizeInPx, sizeInPx),
blendMode = BlendMode.SrcIn
)
restoreToCount(checkPoint)
}
}
In painter code Android team uses it as
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 check alpha to apply layer
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()
}
}
}
}
}

Squirecle Image was not shown on Android 10 devices (Compose)

I am trying to draw Squircle Image using the Android Jetpack Compose.
I found simple code set on Github: https://github.com/Size0f/android.compose.squircle/blob/master/squircle/src/main/java/com/sizeof/libraries/compose/squircle/SquircleShape.kt
Here is SquuircleShape compose:
class SquircleShape : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
) = Outline.Generic(
path = createSquirclePath(size, SMOOTHING)
)
private fun createSquirclePath(size: Size, smoothing: Double): androidx.compose.ui.graphics.Path {
return Path().apply {
val oversize = size.width * OVERSAMPLING_MULTIPLIER
val squircleRadius = (oversize / 2F).toInt()
// power radius before for optimization
val poweredRadius = squircleRadius
.toDouble()
.pow(smoothing)
// generate Y coordinates for path
val yCoordinates = (-squircleRadius..squircleRadius).map { x ->
x.toFloat() to evalSquircleFun(x, poweredRadius, smoothing)
}
// generate Y coordinates for mirror half of squircle shape
val yMirroredCoordinates = yCoordinates.map { (x, y) -> Pair(x, -y) }
var currentX = 0F
var currentY = 0F
// set path by using quadraticBezier
(yCoordinates + yMirroredCoordinates).forEach { (x, y) ->
quadTo(currentX, currentY, x, y)
currentX = x
currentY = y
}
close()
// scale down to original size - for better corners without anti-alias
transform(
scaleMatrix(
sx = 1 / OVERSAMPLING_MULTIPLIER,
sy = 1 / OVERSAMPLING_MULTIPLIER
)
)
// translate path to center
transform(
translationMatrix(
tx = size.width / 2,
ty = size.height / 2
)
)
}.asComposePath()
}
// squircle formula: | (r^smoothing) - |x|^5 | ^ (1 / smoothing)
private fun evalSquircleFun(x: Int, poweredRadius: Double, smoothing: Double) =
(poweredRadius - abs(x.toDouble().pow(smoothing))).pow(1 / smoothing).toFloat()
companion object {
private const val SMOOTHING = 3.0
private const val OVERSAMPLING_MULTIPLIER = 4F
}
}
Here is SquircleImage compose:
#Composable
fun SquircleImage(
modifier: Modifier = Modifier,
imageRequest: ImageRequest,
size: Dp,
backgroundColor: Color = Gray10,
borderColor: Color = Gray20,
borderSize: Dp = 0.5.dp,
radius: Dp = 24.dp
) {
Box(
modifier = modifier
.size(size)
.clip(SquircleShape())
.background(borderColor),
contentAlignment = Alignment.Center
) {
Image(
painter = rememberAsyncImagePainter(
model = imageRequest,
contentScale = ContentScale.Crop
),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(SquircleShape())
.background(backgroundColor)
.size(size - borderSize - borderSize),
)
}
}
In general, it works fine like:
But if I navigate other pages again and again repeatedly, the Squircle images are gone!:
There is no exception or meaningful logs.
It doesn't happen always.
Occurrence frequency maybe 1/10?
And It occurs on Android 10.
Somebody help me, please?

How can I replace rectangles with images/assets using Jetpack Compose [Kotlin]?

I am currently developing a Sokoban game in Kotlin for an Android app and at this point I have an algorithm that generates a matrix of strings where each string refers to a specific game element, e.g., "#" - Wall, "0" - Floor, "#" - Player, "$" - Box, etc...
Based on that matrix, I am using Jetpack Compose to draw in a canvas rectangles with different colors so I can distinguish the elements (wall, floor, ...) of the Sokoban game.
At the moment, this is the result:
Elements represented by rectangles
However, my final goal is to replace that colored rectangles, with images (.png files). Example:
Expected result
How can I do that?
Here's my code:
class SokobanActivity : AppCompatActivity() {
private lateinit var mGameMatrix: Array<Array<String>>
...
#Composable
fun GameCanvas() {
Canvas(
modifier = Modifier
.fillMaxSize()
) {
drawRect(color = Color.Black)
val squareDim = 64.dp.value
(mGameMatrix.indices).forEach { i ->
(mGameMatrix[i].indices).forEach { j ->
val element = mGameMatrix[i][j]
drawGameElement(
squareColor = when (element) {
//Wall
"#" -> Color.Red
//Floor
"0" -> Color.Cyan
//Box
"$" -> Color.Green
//Box-objective
"." -> Color.Yellow
//Player
"#" -> Color.Magenta
else -> Color.Blue
},
Offset(
(i * squareDim) + ((size.width - (mGameMatrix.size * squareDim)) / 2),
(j * squareDim) + ((size.height - (mGameMatrix.size * squareDim)) / 2)
),
size = Size(
width = squareDim,
height = squareDim
)
)
}
}
}
}
private fun DrawScope.drawGameElement(
squareColor: Color,
offset: Offset,
size: Size,
) {
drawRect(
color = squareColor,
topLeft = offset,
size = size
)
}
...
}
You can just draw your vector, png etc. resources on Canvas, like that:
val image = ImageVector.vectorResource(id = iconRes)
val painter = rememberVectorPainter(image = image)
translate(
left = size.width / 2 - painter.intrinsicSize.width / 2,
top = size.height / 2 - painter.intrinsicSize.height / 2
) {
with(painter) {
draw(painter.intrinsicSize)
}
}

Categories

Resources