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
Related
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)
}
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
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?
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 get the actual size values with compose? if something with the layout size has to be done before feeding the data to the view? i don't like checking. when everytime the canvas redraws
Box {
var count by rememberSaveable {
mutableStateOf(0)
}
var states = remember { mutableListOf<Rain>() }
Canvas(modifier = Modifier.fillMaxSize()) {
// i need the canvas' size so i initialize the object inside this scope, and check it every time when canvas redraws
Log.e("canvas", "repainted")
if(states.isEmpty()) states.addAll(MutableList(300) {
Rain(size.width, size.height)
})
drawRect(color = Color.Black)
repeat(300) {
val c = count
val x1 = states[it].x1
val x2 = states[it].x2
val y1 = states[it].y1
val y2 = states[it].y2
val color = states[it].color
drawLine(start = Offset(x1, y1), end = Offset(x2, y2), color = color)
states[it]()
}
}
LaunchedEffect(isPaused) {
while (!isPaused) {
delay(40)
count++
}
}
}
You can save canvasSize into a state variable, and then use derivedStateOf for your states: content of this block will only be recalculated when any of mutable states used inside are changed, in this case it'll be canvasSize
Box {
var count by rememberSaveable {
mutableStateOf(0)
}
var canvasSize by remember { mutableStateOf(Size.Unspecified) }
val states by remember(canvasSize) {
derivedStateOf {
List(300) {
Rain(canvasSize.width, canvasSize.height)
}
}
}
Canvas(
modifier = Modifier
.fillMaxSize()
) {
canvasSize = size
drawRect(color = Color.Blue)
repeat(300) {
val x1 = states[it].x1
val x2 = states[it].x2
val y1 = states[it].y1
val y2 = states[it].y2
drawLine(start = Offset(x1, y1), end = Offset(x2, y2), color = color)
}
states[it]()
}
LaunchedEffect(isPaused) {
while (!isPaused) {
delay(40)
count++
}
}
}
DrawScope where inside has some properties one of them being size which itself has width, height, minDimension and maxDimension properties.
Canvas {
size.width
size.height
}
others are
#DrawScopeMarker
interface DrawScope : Density {
/**
* The current [DrawContext] that contains the dependencies
* needed to create the drawing environment
*/
val drawContext: DrawContext
/**
* Center of the current bounds of the drawing environment
*/
val center: Offset
get() = drawContext.size.center
/**
* Provides the dimensions of the current drawing environment
*/
val size: Size
get() = drawContext.size
/**
* The layout direction of the layout being drawn in.
*/
val layoutDirection: LayoutDirection
...
}