Semi oval shape in Jetpack Compose - android

I am trying to create a custom selectable composable and for the selected indicator, I would like to have a section at the end of the composable with a rounded edge. The final look would be something like this:
What I have currently is a box at the end with the correct background and I am trying to have another box just before which will act as the rounded edge.
The issue is that I am trying to use a GenericShape within the clip modifier and play with the Path. I have use a quadraticBezierTo function but the result isn't great since the top start and bottom start edges are very sharp.
I have looked for some Oval shap in Compose but I haven't found my luck yet.
Here is my current implementation:
#Composable
fun SelectableCard(
information: #Composable RowScope.(modifier: Modifier) -> Unit,
selected: Boolean = false
) {
Card(
modifier = Modifier
.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
) {
information(
modifier = Modifier
.padding(
vertical = dimensionResource(id = R.dimen.spacing_semi_small),
horizontal = dimensionResource(id = R.dimen.spacing_small),
)
)
// Selected indicator
Box(
modifier = Modifier
.fillMaxHeight()
.clip(
GenericShape { size, _ ->
val width: Float = size.width
val height: Float = size.height
// Starting point
moveTo(width, 0f)
quadraticBezierTo(
x1 = 0f,
y1 = height / 2,
x2 = width,
y2 = height
)
}
)
.background(Color.Red)
.width(20.dp)
)
Box(
modifier = Modifier
.fillMaxHeight()
.width(60.dp)
.background(Color.Red),
)
}
}
}

You can use a GenericShape like:
class OvalCornerShape(
size: Dp
) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
val rect = size.toRect()
val path = Path().apply {
addOval(rect)
}
return Outline.Generic(path)
}
}
and apply it to the Box. Something like:
Box(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
) {
// Selected indicator
Box(
modifier = Modifier
.offset(x= 10.dp)
.fillMaxHeight()
.width(60.dp)
.background(Color.Red)
)
Box(
modifier = Modifier
.fillMaxHeight()
.clip(OvalCornerShape(10.dp))
.background(Color.Red)
.width(20.dp)
)
}

Related

How to align a Box in Compose in the Center not just the Content

Here is my Code ! ;) And my Box is aligned at the top left, but I want the box in the center. You know how I can do this?
OptIn(ExperimentalWearMaterialApi::class)
#Composable
fun OnTouch() {
val width = 100.dp
val squareSize = 50.dp
val swipeableState = rememberSwipeableState(0)
val sizePx = with(LocalDensity.current) { squareSize.toPx() }
val anchors = mapOf(0f to 0, sizePx to 1) // Maps anchor points (in px) to states
Box(
contentAlignment = Alignment.Center, modifier = Modifier
.width(width)
.swipeable(
state = swipeableState,
anchors = anchors,
thresholds = { _, _ -> FractionalThreshold(0.3f) },
orientation = Orientation.Horizontal
)
.background(Color.LightGray)
) {
Box(
Modifier
.offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
.size(squareSize)
.background(Color.DarkGray)
)
}
}
thanks in advance ! ;) I did search the documentation but found nothing...
The alignment of the LightGray Box depends on the parent container.
For example you can use a Column like:
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
//...
.background(Color.LightGray)
) {
//...
}
}

adding text above icon in jetpack compose

Is there any way how I can show a little description above specific icon in Jetpack Compose like in this picture?
It's called speech or tooltip bubble. You can create this or any shape using GenericShape or adding RoundedRect.
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp)
) {
var showToolTip by remember {
mutableStateOf(false)
}
Spacer(modifier = Modifier.height(100.dp))
val triangleShape = remember {
GenericShape { size: Size, layoutDirection: LayoutDirection ->
val width = size.width
val height = size.height
lineTo(width / 2, height)
lineTo(width, 0f)
lineTo(0f, 0f)
}
}
Box {
if (showToolTip) {
Column(modifier = Modifier.offset(y = (-48).dp)) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.shadow(2.dp)
.background(Color(0xff26A69A))
.padding(8.dp),
) {
Text("Hello World", color = Color.White)
}
Box(
modifier = Modifier
.offset(x = 15.dp)
.clip(triangleShape)
.width(20.dp)
.height(16.dp)
.background(Color(0xff26A69A))
)
}
}
IconButton(
onClick = { showToolTip = true }
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "null",
Modifier
.background(Color.Red, CircleShape)
.padding(4.dp)
)
}
}
}
If you need shadow or border that must be a single shape you need to build it with GenericShape. You can check my answer out and library i built.
The sample below is simplified version of library, with no Modifier.layout which is essential for setting space reserved for arrow and setting padding correctly instead of creating another Box with Padding
Result
fun getBubbleShape(
density: Density,
cornerRadius: Dp,
arrowWidth: Dp,
arrowHeight: Dp,
arrowOffset: Dp
): GenericShape {
val cornerRadiusPx: Float
val arrowWidthPx: Float
val arrowHeightPx: Float
val arrowOffsetPx: Float
with(density) {
cornerRadiusPx = cornerRadius.toPx()
arrowWidthPx = arrowWidth.toPx()
arrowHeightPx = arrowHeight.toPx()
arrowOffsetPx = arrowOffset.toPx()
}
return GenericShape { size: Size, layoutDirection: LayoutDirection ->
val rectBottom = size.height - arrowHeightPx
this.addRoundRect(
RoundRect(
rect = Rect(
offset = Offset.Zero,
size = Size(size.width, rectBottom)
),
cornerRadius = CornerRadius(cornerRadiusPx, cornerRadiusPx)
)
)
moveTo(arrowOffsetPx, rectBottom)
lineTo(arrowOffsetPx + arrowWidthPx / 2, size.height)
lineTo(arrowOffsetPx + arrowWidthPx, rectBottom)
}
}
Then create a Bubble Composable, i set static values but you can set these as parameters
#Composable
private fun Bubble(
modifier: Modifier = Modifier,
text: String
) {
val density = LocalDensity.current
val arrowHeight = 16.dp
val bubbleShape = remember {
getBubbleShape(
density = density,
cornerRadius = 12.dp,
arrowWidth = 20.dp,
arrowHeight = arrowHeight,
arrowOffset = 30.dp
)
}
Box(
modifier = modifier
.clip(bubbleShape)
.shadow(2.dp)
.background(Color(0xff26A69A))
.padding(bottom = arrowHeight),
contentAlignment = Alignment.Center
) {
Box(modifier = Modifier.padding(8.dp)) {
Text(
text = text,
color = Color.White,
fontSize = 20.sp
)
}
}
}
You can use it as in this sample. You need to change offset of Bubble to match position of ImageButton
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp)
) {
var showToolTip by remember {
mutableStateOf(false)
}
Spacer(modifier = Modifier.height(100.dp))
Box {
if (showToolTip) {
Bubble(
modifier = Modifier.offset(x = (-15).dp, (-52).dp),
text = "Hello World"
)
}
IconButton(
onClick = { showToolTip = true }
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "null",
Modifier
.background(Color.Red, CircleShape)
.padding(4.dp)
)
}
}
}
You can use a Box. The children of the Box layout will be stacked over each other.
Box{
Text(text = "Text Above Icon", modifier = text alignment)
Icon(... , modifier = icon alignment)
}
Text can be added above an icon in Jetpack Compose by using a combination of the Row and Column composables. The Row composable lays out its children in a single row while the Column composable lays out its children in a single column. To add text above the icon, the Row composable should be used first, followed by the Column composable. This will allow the text to be placed on the top of the icon. For example, the following code will add text above an icon:
Row {
Text(text = "Text Above Icon")
Column {
Icon(... )
}
}

Composable fillMaxSize and rotation not working

How can I rotate composables and still make them fill their parent?
For example, I have a Column filling the Screen with two Boxes taking up half the size.
The size of the boxes seems to be calculated before the rotation and not after.
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(
modifier = Modifier
.rotate(90f)
.fillMaxSize()
.weight(1f)
.background(Color.Red),
contentAlignment = Alignment.Center
) {
Text("1", fontSize = 100.sp)
}
Box(
modifier = Modifier
.rotate(90f)
.fillMaxSize()
.weight(1f)
.background(Color.Blue),
contentAlignment = Alignment.Center
) {
Text("2", fontSize = 100.sp)
}
}
Edit after Thracians comment:
This looks better but the maxHeight I get seems wrong, I can see in the layout inspector that the size of the BoxWithConstraints is right
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
BoxWithConstraints(modifier = Modifier.fillMaxSize().weight(1f)) {
Box(
modifier = Modifier
.rotate(90f)
.width(maxHeight)
.height(maxWidth)
.background(Color.Red),
contentAlignment = Alignment.Center
) {
Text("1", fontSize = 100.sp)
}
}
BoxWithConstraints(modifier = Modifier.fillMaxSize().weight(1f)) {
Box(
modifier = Modifier
.rotate(90f)
.width(maxHeight)
.height(maxWidth)
.background(Color.Blue),
contentAlignment = Alignment.Center
) {
Text("2", fontSize = 100.sp)
}
}
Modifier.rotate is
#Stable
fun Modifier.rotate(degrees: Float) =
if (degrees != 0f) graphicsLayer(rotationZ = degrees) else this
Modifier.graphicsLayer{} does not change dimensions and physical position of a Composable, and doesn't trigger recomposition which is very good for animating or changing visual presentation.
You can also see in my question here even i change scale and position green rectangle, position in parent is not changing.
A [Modifier.Element] that makes content draw into a draw layer. The
draw layer can be * invalidated separately from parents. A
[graphicsLayer] should be used when the content * updates
independently from anything above it to minimize the invalidated
content. * * [graphicsLayer] can also be used to apply effects to
content, such as scaling ([scaleX], [scaleY]), * rotation
([rotationX], [rotationY], [rotationZ]), opacity ([alpha]), shadow
However any Modifier after Modifier.graphicsLayer is applied based on new scale, translation or rotation. Easiest to see is drawing border before and after graphicsLayer.
Column(modifier = Modifier.fillMaxSize()) {
val context = LocalContext.current
Row(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.size(100.dp, 200.dp)
.border(2.dp, Color.Red)
.zIndex(1f)
.clickable {
Toast
.makeText(context, "Before Layer", Toast.LENGTH_SHORT)
.show()
}
.graphicsLayer {
translationX = 300f
rotationZ = 90f
}
.border(2.dp, Color.Green)
.clickable {
Toast
.makeText(context, "After Layer", Toast.LENGTH_SHORT)
.show()
}
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.Yellow)
)
}
}
If you check this example you will see that even after we rotate Composable on left position of Box with yellow background doesn't change because we don't change actual form of Composable on left side.
#Composable
private fun MyComposable() {
Column(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.height(300.dp))
Column(modifier = Modifier.fillMaxSize()) {
var angle by remember { mutableStateOf(0f) }
LaunchedEffect(key1 = Unit) {
delay(2000)
angle = 90f
}
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
.background(Color.Red)
.rotate(angle),
contentAlignment = Alignment.Center
) {
Text("1", fontSize = 100.sp)
}
Spacer(modifier =Modifier.height(8.dp))
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
.background(Color.Blue)
.rotate(angle),
contentAlignment = Alignment.Center
) {
Text("2", fontSize = 100.sp)
}
}
}
}
Added Spacer to have uneven width and height for Boxes for demonstration.
As i posted in example gif order of rotate determines what we rotate.
modifier = Modifier
.fillMaxSize()
.weight(1f)
.background(Color.Blue)
.rotate(angle)
This sets the size, it never rotates parent but the content or child because we set size and background before rotation. This answer works if width of the child is not greater than height of the parent.
If child has Modifier.fillSize() or child's width is bigger than parent's height when we rotate as in the image left below. So we need to scale it back to parent after rotation since we didn't change parents dimensions.
#Composable
private fun MyComposable2() {
var angle by remember { mutableStateOf(0f) }
LaunchedEffect(key1 = Unit) {
delay(2000)
angle = 90f
}
Column(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.height(100.dp))
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
) {
val width = maxWidth
val height = maxHeight / 2
val newWidth = if (angle == 0f) width else height
val newHeight = if (angle == 0f) height else width
Column(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.border(3.dp, Color.Red)
.size(width, height)
.graphicsLayer {
rotationZ = angle
scaleX = newWidth/width
scaleY = newHeight/height
}
.border(5.dp, Color.Yellow),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier
.border(4.dp, getRandomColor())
.fillMaxSize(),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = "",
contentScale = ContentScale.FillBounds
)
}
Box(
modifier = Modifier
.border(3.dp, Color.Red)
.graphicsLayer {
rotationZ = angle
scaleX = newWidth/width
scaleY = newHeight/height
}
.size(width, height)
.border(5.dp, Color.Yellow),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier
.border(4.dp, getRandomColor())
.fillMaxSize(),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = "",
contentScale = ContentScale.FillBounds
)
}
}
}
}
}
on left we don't scale only rotate as in question
on right we scale child into parent based on height/width ratio

How can I make all cells of a Row have the width of the widest one?

I am making a "ToggleGroup" with Jetpack-Compose, using essentially a Row into which I print Text. I manage to make it work if I tune the width manually (.width(70.dp) in the code below), but I would like it to automatically do that.
I essentially want this:
But without manually adding .width(70.dp), I get this:
My current (tuned) code is the following:
Row(
horizontalArrangement = Arrangement.End,
) {
options.forEach { option ->
val isSelected = option == selectedOption
val textColor = if (isSelected) Color.White else MaterialTheme.colors.primary
val backgroundColor = if (isSelected) Color.Gray else Color.White
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.padding(
vertical = 6.dp, horizontal = 1.dp
)
.width(70.dp)
.background(backgroundColor)
.clickable { onSelectionChanged(option) }
) {
Text(
text = option,
color = textColor,
modifier = Modifier.padding(14.dp),
)
}
}
}
It feels similar to this question, but somehow it's different because I use Row and the question uses Column (or at least I did not manage to use Intrinsics correctly).
How could I do that?
Changes required.
1. On the parent Row, use Modifier.width(IntrinsicSize.Min)
(min|max)IntrinsicWidth: Given this height, what's the minimum/maximum width you can paint your content properly?
Source - Docs
2. Use Modifier.weight(1F) on all children.
Size the element's width proportional to its weight relative to other weighted sibling elements in the Row.
The parent will divide the horizontal space remaining after measuring unweighted child elements and distribute it according to this weight.
Source - Docs
3. Use Modifier.width(IntrinsicSize.Max) on all children.
This ensures the Text inside the children composables are not wrapped.
(You can verify this by removing the modifier and adding long text)
Screenshot
Sample code
#Composable
fun AutoWidthRow() {
val items = listOf("Item 1", "Item 2", "Item 300")
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.width(IntrinsicSize.Min),
) {
items.forEach { option ->
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.padding(
vertical = 6.dp, horizontal = 1.dp
)
.width(IntrinsicSize.Max) // Removing this will wrap the text
.weight(1F)
.background(Color.Black)
) {
Text(
text = option,
color = Color.White,
modifier = Modifier
.padding(14.dp),
)
}
}
}
}
You should create a custom Layout
#Composable
fun EqualSizeTiles(
modifier: Modifier = Modifier,
content: #Composable () -> Unit,
) {
Layout(
content = content,
modifier = modifier,
) { measurables, constraints ->
layoutTiles(
measurables,
constraints
)
}
}
private fun MeasureScope.layoutTiles(
measurables: List<Measurable>,
constraints: Constraints,
): MeasureResult {
val tileHeight = constraints.maxHeight
val tileWidths = measurables.map { measurable ->
measurable.maxIntrinsicWidth(tileHeight)
}
val tileWidth = tileWidths.maxOrNull() ?: 0
val tileConstraints = Constraints(
minWidth = tileWidth,
minHeight = 0,
maxWidth = tileWidth,
maxHeight = constraints.maxHeight,
)
val placeables = measurables.map { measurable ->
measurable.measure(tileConstraints)
}
val width = (placeables.size * tileWidth).coerceAtMost(constraints.maxWidth)
return layout(width = width, height = tileHeight) {
placeables.forEachIndexed { index, placeable ->
placeable.place(tileWidth * index, 0)
}
}
}
#Preview(showBackground = true, widthDp = 512)
#Composable
private fun EqualSizeTilesPreview() {
WeatherSampleTheme {
Surface(
modifier = Modifier
.fillMaxWidth()
.background(color = Color.Yellow)
) {
EqualSizeTiles(
modifier = Modifier
.height(64.dp)
.background(color = Color.Green)
.padding(all = 8.dp)
) {
Text(
text = "Left",
textAlign = TextAlign.Center,
modifier = Modifier
.background(color = Color.Red)
.padding(all = 8.dp)
.fillMaxHeight(),
)
Text(
text = "Center",
textAlign = TextAlign.Center,
modifier = Modifier
.background(color = Color.Yellow)
.padding(all = 8.dp)
.fillMaxHeight(),
)
Text(
text = "Right element",
textAlign = TextAlign.Center,
modifier = Modifier
.background(color = Color.Blue)
.padding(all = 8.dp)
.fillMaxHeight(),
)
}
}
}
}

Jetpack Compose: expand image with restricted height

I want to show a list of elements in which each one is an expanded image on top and a title on the bottom.
The image must expand all of its available width but it must have a fixed height.
If the image is big enough I can do it, but if the image has a portrait aspect ratio it doesn't expand as I would like.
This is what I have right now:
The first image has a portrait aspect ratio, I would like it to expand to the available width but maintaining a fixed height.
This is the code for the list and each element:
#Composable
private fun List(memes: List<Meme>) {
LazyColumn(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
itemsIndexed(memes) { index, meme ->
ListElement(meme, isFirst = index == 0, isLast = index == memes.size - 1)
}
}
}
#Composable
private fun ListElement(meme: Meme, isFirst: Boolean, isLast: Boolean) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(
start = marginLat,
top = if (isFirst) 8.dp else 0.dp,
end = marginLat,
bottom = if (isLast) 8.dp else 0.dp
),
elevation = 4.dp
) {
Column {
Image(
painter = rememberImagePainter(meme.url),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.padding(8.dp)
.clip(RoundedCornerShape(4.dp))
)
Text(meme.name, style = MaterialTheme.typography.h6, modifier = Modifier.padding(8.dp))
}
}
}
use ContentScale.FillWidth
Image(
painter = rememberImagePainter(meme.url),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.padding(8.dp)
.clip(RoundedCornerShape(4.dp))
)

Categories

Resources