I need set Diagonal gradient on a rectangle as background.
I have two colors (Yellow and Green) which needs to be painted as:
Green from top left to bottom right and Yellow from bottom right to topleft.
I see only linearGradient, horizontalGradient and verticalGradient on Brush in Modifier.
But I'm not able to generate the required angle.
Brush.linearGradient() is diagonal by default with default angle 45 degrees. You can change angle of gradient by changing start and values. Rotating by 45 is easy.
I add a demonstration. You can pick start and end values for any angle from GradientOffset function and paste it to Brush.linearGradient
/**
* Offset for [Brush.linearGradient] to rotate gradient depending on [start] and [end] offsets.
*/
data class GradientOffset(val start: Offset, val end: Offset)
enum class GradientAngle {
CW0, CW45, CW90, CW135, CW180, CW225, CW270, CW315
}
fun GradientOffset(angle: GradientAngle = GradientAngle.CW0): GradientOffset {
return when (angle) {
GradientAngle.CW45 -> GradientOffset(
start = Offset.Zero,
end = Offset.Infinite
)
GradientAngle.CW90 -> GradientOffset(
start = Offset.Zero,
end = Offset(0f, Float.POSITIVE_INFINITY)
)
GradientAngle.CW135 -> GradientOffset(
start = Offset(Float.POSITIVE_INFINITY, 0f),
end = Offset(0f, Float.POSITIVE_INFINITY)
)
GradientAngle.CW180 -> GradientOffset(
start = Offset(Float.POSITIVE_INFINITY, 0f),
end = Offset.Zero,
)
GradientAngle.CW225 -> GradientOffset(
start = Offset.Infinite,
end = Offset.Zero
)
GradientAngle.CW270 -> GradientOffset(
start = Offset(0f, Float.POSITIVE_INFINITY),
end = Offset.Zero
)
GradientAngle.CW315 -> GradientOffset(
start = Offset(0f, Float.POSITIVE_INFINITY),
end = Offset(Float.POSITIVE_INFINITY, 0f)
)
else -> GradientOffset(
start = Offset.Zero,
end = Offset(Float.POSITIVE_INFINITY, 0f)
)
}
}
Demonstration of how gradients are formed based on start and end values
#Composable
private fun RotatableGradientSample() {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Offsets for gradients based on selected angle
var gradientOffset by remember {
mutableStateOf(GradientOffset(GradientAngle.CW45))
}
var angleSelection by remember { mutableStateOf(0f) }
var angleText by remember { mutableStateOf("0 Degrees") }
val brush = Brush.linearGradient(
listOf(Color.Green, Color.Yellow),
start = gradientOffset.start,
end = gradientOffset.end
)
Text(
text = angleText,
color = Color.Red,
modifier = Modifier
.padding(8.dp),
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Slider(
modifier = Modifier.height(50.dp),
value = angleSelection,
onValueChange = {
angleSelection = it
gradientOffset = when (angleSelection.roundToInt()) {
0 -> {
angleText = "0 Degrees"
GradientOffset(GradientAngle.CW0)
}
1 -> {
angleText = "45 Degrees"
GradientOffset(GradientAngle.CW45)
}
2 -> {
angleText = "90 Degrees"
GradientOffset(GradientAngle.CW90)
}
3 -> {
angleText = "135 Degrees"
GradientOffset(GradientAngle.CW135)
}
4 -> {
angleText = "180 Degrees"
GradientOffset(GradientAngle.CW180)
}
5 -> {
angleText = "225 Degrees"
GradientOffset(GradientAngle.CW225)
}
6 -> {
angleText = "270 Degrees"
GradientOffset(GradientAngle.CW270)
}
else -> {
angleText = "315 Degrees"
GradientOffset(GradientAngle.CW315)
}
}
},
steps = 6,
valueRange = 0f..7f
)
Spacer(modifier = Modifier.height(10.dp))
Box(
Modifier
.fillMaxWidth(.4f)
.aspectRatio(5 / 3f)
.background(brush)
)
Spacer(modifier = Modifier.height(10.dp))
Box(
Modifier
.fillMaxWidth(.4f)
.aspectRatio(1f)
.background(brush)
)
Spacer(modifier = Modifier.height(10.dp))
Box(
Modifier
.fillMaxWidth(.4f)
.aspectRatio(1f)
.background(brush, CircleShape)
)
}
}
You can try:
Box(
modifier = Modifier
.size(100.dp, 50.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color.Yellow,
Color.Green
)
)
)
)
Related
In the shopping list item composable, I am reducing the alpha value on the whole item by changing the alpha value on it's parent Row composable, but I want to exclude a child Icon composable from receiving the parent Row's alpha change and retain a 100% alpha value. I set the modifier on the child Icon to alpha(1f), but it is not working. The alpha change on the parent is also propagating to the child despite this. Is it possible exclude the child from the parent's alpha change?
Composable
#Composable
fun ShoppingListScreenItem(
rowModifier: Modifier = Modifier,
item: ShoppingListItem,
mainViewModel: ShoppingListScreenViewModel,
onNavigateToAddEditItemScreenFromItemStrip: (ShoppingListItem) -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 3.dp, bottom = 5.dp)
.then(rowModifier)
.alpha(if (item.isItemDisabled) 0.35f else 1f)
.clickable {
onNavigateToAddEditItemScreenFromItemStrip(item)
},
verticalAlignment = Alignment.CenterVertically,
) {
...
if(mainViewModel.shoppingListState.value!!.sortOrder != "Custom" && !item.isInCart) {
//I want to exclude this child composable
IconToggleButton(
checked = item.isItemDisabled,
onCheckedChange = {
scope.launch {
mainViewModel.updateShoppingListItemDisabledInDb(item, it)
}
},
) {
Icon(
modifier = Modifier.alpha(1f).size(26.dp),
painter = painterResource(id = R.drawable.baseline_disabled_visible_24),
contentDescription = "Toggle disable the item strip",
)
}
}
}
}
This can be achieved using Layout and placeable.placeWithLayer and Modifier.layoutId to select which Composable is to be used with default alpha as.
This is a custom Column, you can customize Layout as required, purpose is to show Modifier.layoutId usage and Placeable.placeRelativeWithLayer to apply any desired graphic layer property to specific Composable in layout phase.
Result
Usage
MyLayout(
alpha = .5f
) {
Text("Default Alpha", fontSize = 20.sp)
Text("Default Alpha", fontSize = 20.sp)
Text("Custom Alpha", fontSize = 20.sp), modifier = Modifier.layoutId("full_alpha"))
Text("Default Alpha", fontSize = 20.sp)
Image(painter = painterResource(id = R.drawable.landscape5), contentDescription = "")
}
Implementation
#Composable
private fun MyLayout(
modifier: Modifier = Modifier,
alpha: Float = 1f,
content: #Composable () -> Unit
) {
val measurePolicy = MeasurePolicy { measurables, constraints ->
val fullAlphaIndex = measurables.indexOfFirst {
it.layoutId == "full_alpha"
}
val placeablesWidth = measurables.map { measurable ->
measurable.measure(constraints)
}
val hasBoundedWidth = constraints.hasBoundedWidth
val hasFixedWidth = constraints.hasFixedWidth
val width =
if (hasBoundedWidth && hasFixedWidth) constraints.maxWidth
else placeablesWidth.maxOf { it.width }
val height = placeablesWidth.sumOf {
it.height
}
var posY = 0
layout(width, height) {
placeablesWidth.forEachIndexed { index, placeable ->
placeable.placeRelativeWithLayer(0, posY) {
if (index == fullAlphaIndex) {
this.alpha = 1f
} else {
this.alpha = alpha
}
}
posY += placeable.height
}
}
}
Layout(
modifier = modifier,
content = content,
measurePolicy = measurePolicy
)
}
If you wish to create a Row you need to place items one after other horizontally instead of increasing y position
Result
#Composable
fun MyLayout(
modifier: Modifier = Modifier,
alpha: Float = 1f,
content: #Composable () -> Unit
) {
val measurePolicy = MeasurePolicy { measurables, constraints ->
val fullAlphaIndex = measurables.indexOfFirst {
it.layoutId == "full_alpha"
}
val placeablesWidth = measurables.map { measurable ->
measurable.measure(
constraints.copy(
minWidth = 0,
maxWidth = Constraints.Infinity,
minHeight = 0,
maxHeight = Constraints.Infinity
)
)
}
val hasBoundedWidth = constraints.hasBoundedWidth
val hasFixedWidth = constraints.hasFixedWidth
val hasBoundedHeight = constraints.hasBoundedHeight
val hasFixedHeight = constraints.hasFixedHeight
val width =
if (hasBoundedWidth && hasFixedWidth) constraints.maxWidth
else placeablesWidth.sumOf { it.width }.coerceAtMost(constraints.maxWidth)
val height =
if (hasBoundedHeight && hasFixedHeight) constraints.maxHeight
else placeablesWidth.maxOf { it.height }.coerceAtMost(constraints.maxHeight)
var posX = 0
layout(width, height) {
placeablesWidth.forEachIndexed { index, placeable ->
placeable.placeRelativeWithLayer(posX, 0) {
if (index == fullAlphaIndex) {
this.alpha = 1f
} else {
this.alpha = alpha
}
}
posX += placeable.width
}
}
}
Layout(
modifier = modifier,
content = content,
measurePolicy = measurePolicy
)
}
Same usage but this time with Icons
MyLayout(
modifier = Modifier.drawChecker(),
alpha = .4f
) {
Icon(
imageVector = Icons.Default.NotificationsActive,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.background(Color.Red, CircleShape)
.size(100.dp)
.padding(10.dp)
)
Icon(
imageVector = Icons.Default.NotificationsActive,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.background(Color.Red, CircleShape)
.size(100.dp)
.padding(10.dp)
)
Icon(
imageVector = Icons.Default.NotificationsActive,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.layoutId("full_alpha")
.background(Color.Red, CircleShape)
.size(100.dp)
.padding(10.dp)
)
Icon(
imageVector = Icons.Default.NotificationsActive,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.background(Color.Red, CircleShape)
.size(100.dp)
.padding(10.dp)
)
}
Checker Modifier if anyone wonders is as
fun Modifier.drawChecker() = this.then(
drawBehind {
val width = this.size.width
val height = this.size.height
val checkerWidth = 10.dp.toPx()
val checkerHeight = 10.dp.toPx()
val horizontalSteps = (width / checkerWidth).toInt()
val verticalSteps = (height / checkerHeight).toInt()
for (y in 0..verticalSteps) {
for (x in 0..horizontalSteps) {
val isGrayTile = ((x + y) % 2 == 1)
drawRect(
color = if (isGrayTile) Color.LightGray else Color.White,
topLeft = Offset(x * checkerWidth, y * checkerHeight),
size = Size(checkerWidth, checkerHeight)
)
}
}
}
)
How do I create a Arc Progress bar animation like this
Currently I've already used Canvas to draw an arc and added animations to the progress bar using animateFloatAsState API. But second pic is not my expected.
[]
// e.g. oldScore = 100f newScore = 350f
// Suppose 250 points are into one level
#Composable
fun ArcProgressbar(
modifier: Modifier = Modifier,
oldScore: Float,
newScore: Float,
level: String,
startAngle: Float = 120f,
limitAngle: Float = 300f,
thickness: Dp = 8.dp
) {
var value by remember { mutableStateOf(oldScore) }
val sweepAngle = animateFloatAsState(
targetValue = (value / 250) * limitAngle, // convert the value to angle
animationSpec = tween(
durationMillis = 1000
)
)
LaunchedEffect(Unit) {
delay(1500)
value = newScore
}
Box(modifier = modifier.fillMaxWidth()) {
Canvas(
modifier = Modifier
.fillMaxWidth(0.45f)
.padding(10.dp)
.aspectRatio(1f)
.align(Alignment.Center),
onDraw = {
// Background Arc
drawArc(
color = Gray100,
startAngle = startAngle,
sweepAngle = limitAngle,
useCenter = false,
style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
size = Size(size.width, size.height)
)
// Foreground Arc
drawArc(
color = Green500,
startAngle = startAngle,
sweepAngle = sweepAngle.value,
useCenter = false,
style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
size = Size(size.width, size.height)
)
}
)
Text(
text = level,
modifier = Modifier
.fillMaxWidth(0.125f)
.align(Alignment.Center)
.offset(y = (-10).dp),
color = Color.White,
fontSize = 82.sp
)
Text(
text = "LEVEL",
modifier = Modifier
.padding(bottom = 8.dp)
.align(Alignment.BottomCenter),
color = Color.White,
fontSize = 20.sp
)
}
}
How can I animate from start again if progress percentage over 100%, just like the one in the gif. Does anybody got some ideas? Thanks!
My first answer doesn't feel like doing any justice since it's far from the gif you posted which shows what you want.
So here's another one that closely resembles it. However, I feel like this implementation is not very efficient in terms of calling sequences of animations, but in terms of re-composition I incorporated some optimization strategy called deferred reading, making sure only the composables that observes the values will be the only parts that will be re-composed. I left a Log statement in the parent progress composable to verify it, the ArcProgressbar is not updating unnecessarily when the progress is animating.
Log.e("ArcProgressBar", "Recomposed")
Full source code that you can copy-and-paste (preferably on a separate file) without any issues.
val maxProgressPerLevel = 200 // you can change this to any max value that you want
val progressLimit = 300f
fun calculate(
score: Float,
level: Int,
) : Float {
return (abs(score - (maxProgressPerLevel * level)) / maxProgressPerLevel) * progressLimit
}
#Composable
fun ArcProgressbar(
modifier: Modifier = Modifier,
score: Float
) {
Log.e("ArcProgressBar", "Recomposed")
var level by remember {
mutableStateOf(score.toInt() / maxProgressPerLevel)
}
var targetAnimatedValue = calculate(score, level)
val progressAnimate = remember { Animatable(targetAnimatedValue) }
val scoreAnimate = remember { Animatable(0f) }
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(level, score) {
if (score > 0f) {
// animate progress
coroutineScope.launch {
progressAnimate.animateTo(
targetValue = targetAnimatedValue,
animationSpec = tween(
durationMillis = 1000
)
) {
if (value >= progressLimit) {
coroutineScope.launch {
level++
progressAnimate.snapTo(0f)
}
}
}
}
// animate score
coroutineScope.launch {
if (scoreAnimate.value > score) {
scoreAnimate.snapTo(0f)
}
scoreAnimate.animateTo(
targetValue = score,
animationSpec = tween(
durationMillis = 1000
)
)
}
}
}
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box {
PointsProgress(
progress = {
progressAnimate.value // deferred read of progress
}
)
CollectorLevel(
modifier = Modifier.align(Alignment.Center),
level = {
level + 1 // deferred read of level
}
)
}
CollectorScore(
modifier = Modifier.padding(top = 16.dp),
score = {
scoreAnimate.value // deferred read of score
}
)
}
}
#Composable
fun CollectorScore(
modifier : Modifier = Modifier,
score: () -> Float
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Collector Score",
color = Color.White,
fontSize = 16.sp
)
Text(
text = "${score().toInt()} PTS",
color = Color.White,
fontSize = 40.sp
)
}
}
#Composable
fun CollectorLevel(
modifier : Modifier = Modifier,
level: () -> Int
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier
.padding(top = 16.dp),
text = level().toString(),
color = Color.White,
fontSize = 82.sp
)
Text(
text = "LEVEL",
color = Color.White,
fontSize = 16.sp
)
}
}
#Composable
fun BoxScope.PointsProgress(
progress: () -> Float
) {
val start = 120f
val end = 300f
val thickness = 8.dp
Canvas(
modifier = Modifier
.fillMaxWidth(0.45f)
.padding(10.dp)
.aspectRatio(1f)
.align(Alignment.Center),
onDraw = {
// Background Arc
drawArc(
color = Color.LightGray,
startAngle = start,
sweepAngle = end,
useCenter = false,
style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
size = Size(size.width, size.height)
)
// Foreground Arc
drawArc(
color = Color(0xFF3db39f),
startAngle = start,
sweepAngle = progress(),
useCenter = false,
style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
size = Size(size.width, size.height)
)
}
)
}
Sample usage:
#Composable
fun PrizeProgressScreen() {
var score by remember {
mutableStateOf(0f)
}
var scoreInput by remember {
mutableStateOf("0")
}
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF6b4cba)),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier
.padding(vertical = 16.dp),
text = "Progress for every level up: $maxProgressPerLevel",
color = Color.LightGray,
fontSize = 16.sp
)
ArcProgressbar(
score = score,
)
Button(onClick = {
score += scoreInput.toFloat()
}) {
Text("Add Score")
}
TextField(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
value = scoreInput,
onValueChange = {
scoreInput = it
}
)
}
}
I made some changes in your code to utilize Animatable so we always snap to the beginning before animating to our target value. We also eliminated the computation here since we just want to fill the entire progress every time the score updates, in our case to 300 (limitAngle) and used the newScore state as a key in the LaunchedEffect to trigger the animation every time it increments. Don't mind the +30 increments, its just an arbitrary value that you can change without affecting the animation.
#Composable
fun ArcProgressbar(
modifier: Modifier = Modifier,
newScore: Float,
level: String,
startAngle : Float = 120f,
limitAngle: Float = 300f,
thickness: Dp = 8.dp
) {
val animateValue = remember { Animatable(0f) }
LaunchedEffect(newScore) {
if (newScore > 0f) {
animateValue.snapTo(0f)
delay(10)
animateValue.animateTo(
targetValue = limitAngle,
animationSpec = tween(
durationMillis = 1000
)
)
}
}
Box(modifier = modifier.fillMaxWidth()) {
Canvas(
modifier = Modifier
.fillMaxWidth(0.45f)
.padding(10.dp)
.aspectRatio(1f)
.align(Alignment.Center),
onDraw = {
// Background Arc
drawArc(
color = Color.Gray,
startAngle = startAngle,
sweepAngle = limitAngle,
useCenter = false,
style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
size = Size(size.width, size.height)
)
// Foreground Arc
drawArc(
color = Color.Green,
startAngle = startAngle,
sweepAngle = animateValue.value,
useCenter = false,
style = Stroke(thickness.toPx(), cap = StrokeCap.Square),
size = Size(size.width, size.height)
)
}
)
Column {
Text(
text = level,
modifier = Modifier
.fillMaxWidth(0.125f)
.offset(y = (-10).dp),
color = Color.Gray,
fontSize = 82.sp
)
Text(
text = "LEVEL",
modifier = Modifier
.padding(bottom = 8.dp),
color = Color.Gray,
fontSize = 20.sp
)
Text(
text = "Score ( $newScore ) ",
modifier = Modifier
.padding(bottom = 8.dp),
color = Color.Gray,
fontSize = 20.sp
)
}
}
}
Sample usage:
#Composable
fun ScoreGenerator() {
var newScore by remember {
mutableStateOf(0f)
}
Column {
Button(onClick = {
newScore += 30f
}) {
Text("Add Score + 30")
}
ArcProgressbar(
newScore = newScore,
level = ""
)
}
}
I'm creating a 2D game with simple animations in jetpack compose. The game is simple, a balloon will float on top, a cannon will shoot arrow towards balloon. If the arrow hits balloon, player gets a point. I'm able to animate all of the above things but one. How would I detect when the arrow and balloon areas intersect?
The arrow is moving upwards with animation, also the balloon is animating side-ways. I want to capture the point at which the arrow touches the balloon. How would I proceed with this?
CODE:
Balloon Composable
#Composable
fun Balloon(modifier: Modifier, gameState: GameState) {
val localConfig = LocalConfiguration.current
val offsetX by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenWidthDp.dp - 80.dp),
infiniteRepeatable(
animation = tween((Math.random() * 1000).toInt(), easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val offsetY by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenHeightDp.dp / 3),
infiniteRepeatable(
animation = tween((Math.random() * 1000).toInt(), easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Box(
modifier = modifier
.offset(offsetX, offsetY)
.size(80.dp)
.background(Color.Red, shape = CircleShape),
contentAlignment = Alignment.Center,
) {
Text(text = "D!!")
}
}
Cannon Composable:
#Composable
fun ShootBalloon(modifier: Modifier) {
val localConfig = LocalConfiguration.current
var gameState by remember { mutableStateOf(GameState.START) }
val offsetX by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (localConfig.screenWidthDp.dp - 50.dp),
infiniteRepeatable(
animation = tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val offsetY by animateDpAsState(
targetValue = if (gameState == GameState.START) 0.dp else (-(localConfig.screenHeightDp.dp - 50.dp)),
infiniteRepeatable(
animation = tween(500, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
Column(
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.Start
) {
if (gameState == GameState.START) {
Button(onClick = { gameState = GameState.PLAYING }) { Text(text = "Play") }
}
Box(
modifier = Modifier
.size(50.dp)
.offset(x = offsetX)
.background(Color.Green),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(R.drawable.ic_baseline_arrow_upward_24),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.offset(y = offsetY)
.onGloballyPositioned { layoutCoordinates ->
val offset = layoutCoordinates.positionInRoot()
}
)
}
}
}
I'm animating following things,
The cannon box moves side ways, the arrow image moves upwards.
The balloon has random movement.
Add a ticker and on each tick check the global coordinate of arrow and offsets of balloon. If values are same, it means they hit on another
I have a page like this:
When one box is focused, it will be scaled. I use Modifier.graphicsLayer() to scale it.
but the scaled box will be covered by other boxes(box01 is covered by box02,box04 and box 05)
what I actually need is: the scaled box covers other boxes,like this:
My Sample Code:
#Composable
fun FocusBox(
title:String,
requester: FocusRequester = FocusRequester(),
modifier: Modifier = Modifier
) {
var boxColor by remember { mutableStateOf(Color.White) }
var scale by remember { mutableStateOf(1f) }
Box(
Modifier
.focusRequester(requester)
.onFocusChanged {
boxColor = if (it.isFocused) Color.Green else Color.Gray
scale = if (it.isFocused) { 1.3f } else { 1f }
}
.focusable()
.graphicsLayer(
scaleX = scale,
scaleY = scale
).background(boxColor)
) {
Text(
text = title,
modifier = Modifier.padding(30.dp),
color = Color.White,
style = MaterialTheme.typography.subtitle2
)
}
}
#Composable
fun FocusScaleBoxDemo(){
Row(modifier = Modifier.padding(30.dp)){
Column{
FocusBox(title = "Box_01")
Spacer(modifier = Modifier.padding(5.dp))
FocusBox(title = "Box_02")
Spacer(modifier = Modifier.padding(5.dp))
FocusBox(title = "Box_03")
Spacer(modifier = Modifier.padding(5.dp))
}
Spacer(modifier = Modifier.padding(5.dp))
Column{
FocusBox(title = "Box_04")
Spacer(modifier = Modifier.padding(5.dp))
FocusBox(title = "Box_05")
Spacer(modifier = Modifier.padding(5.dp))
FocusBox(title = "Box_06")
Spacer(modifier = Modifier.padding(5.dp))
}
}
}
Basically you need zIndex to bring view under neighbours. But this modifier only works for one container. So if you only add it to the selected box, neighbour column will still be on top of that. You need to add it to the Column containing selected box too.
I also prettified you code a little bit: try to avoid code repetition as much as possible - you'll decrease mistake chances and increase modifications speed
#Composable
fun FocusScaleBoxDemo() {
val columnsCount = 2
val rowsCount = 3
var focusedColumnIndex by remember { mutableStateOf(0) }
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(30.dp)
) {
for (column in 0 until columnsCount) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier
.zIndex(if (column == focusedColumnIndex) 1f else 0f)
) {
for (row in 0 until rowsCount) {
val boxIndex = column * rowsCount + row
FocusBox(
title = "Box_${boxIndex + 1}",
onFocused = {
focusedColumnIndex = column
},
)
}
}
}
}
}
#Composable
fun FocusBox(
title: String,
onFocused: () -> Unit,
requester: FocusRequester = remember { FocusRequester() },
) {
var isFocused by remember { mutableStateOf(false) }
val scale = if (isFocused) 1.3f else 1f
Box(
Modifier
.focusRequester(requester)
.onFocusChanged {
isFocused = it.isFocused
if (isFocused) {
onFocused()
}
}
.focusable()
.graphicsLayer(
scaleX = scale,
scaleY = scale
)
.background(if (isFocused) Color.Green else Color.Gray)
.zIndex(if (isFocused) 1f else 0f)
) {
Text(
text = title,
modifier = Modifier.padding(30.dp),
color = Color.White,
style = MaterialTheme.typography.subtitle2
)
}
}
I want to add border on bottom of the layout. I know i can use Divider composable but i just want to learn how to draw a border.
Currently, I can add border for all sides which is not what I want.
Row(
modifier = Modifier
.border(border = BorderStroke(width = 1.dp, Color.LightGray))
) {
TextField(value = "", onValueChange = {}, modifier = Modifier.weight(1f))
Switch(checked = true, onCheckedChange = {})
Icon(Icons.Filled.Close, "Remove", tint = Color.Gray)
}
You can use the drawBehind modifier to draw a line.
Something like:
Row(
modifier = Modifier
.drawBehind {
val strokeWidth = indicatorWidth.value * density
val y = size.height - strokeWidth / 2
drawLine(
Color.LightGray,
Offset(0f, y),
Offset(size.width, y),
strokeWidth
)
}){
//....
}
If you prefer you can build your custom Modifier with the same code above
fun Modifier.bottomBorder(strokeWidth: Dp, color: Color) = composed(
factory = {
val density = LocalDensity.current
val strokeWidthPx = density.run { strokeWidth.toPx() }
Modifier.drawBehind {
val width = size.width
val height = size.height - strokeWidthPx/2
drawLine(
color = color,
start = Offset(x = 0f, y = height),
end = Offset(x = width , y = height),
strokeWidth = strokeWidthPx
)
}
}
)
and then just apply it:
Row(
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth()
.bottomBorder(1.dp, DarkGray)
){
//Row content
}
You can draw a line in a draw scope. In my opinion, a divider looks cleaner in code.
Row(modifier = Modifier
.drawWithContent {
drawContent()
clipRect { // Not needed if you do not care about painting half stroke outside
val strokeWidth = Stroke.DefaultMiter
val y = size.height // - strokeWidth
// if the whole line should be inside component
drawLine(
brush = SolidColor(Color.Red),
strokeWidth = strokeWidth,
cap = StrokeCap.Square,
start = Offset.Zero.copy(y = y),
end = Offset(x = size.width, y = y)
)
}
}
) {
Text("test")
}
Yeah this oughta do it:-
#Suppress("UnnecessaryComposedModifier")
fun Modifier.topRectBorder(width: Dp = Dp.Hairline, brush: Brush = SolidColor(Color.Black)): Modifier = composed(
factory = {
this.then(
Modifier.drawWithCache {
onDrawWithContent {
drawContent()
drawLine(brush, Offset(width.value, 0f), Offset(size.width - width.value, 0f))
}
}
)
},
inspectorInfo = debugInspectorInfo {
name = "border"
properties["width"] = width
if (brush is SolidColor) {
properties["color"] = brush.value
value = brush.value
} else {
properties["brush"] = brush
}
properties["shape"] = RectangleShape
}
)
You can define a rectangular Shape on the bottom of your element, using the bottom line thickness as parameter:
private fun getBottomLineShape(bottomLineThickness: Float) : Shape {
return GenericShape { size, _ ->
// 1) Bottom-left corner
moveTo(0f, size.height)
// 2) Bottom-right corner
lineTo(size.width, size.height)
// 3) Top-right corner
lineTo(size.width, size.height - bottomLineThickness)
// 4) Top-left corner
lineTo(0f, size.height - bottomLineThickness)
}
}
And then use it in the border modifier like this:
val lineThickness = with(LocalDensity.current) {[desired_thickness_in_dp].toPx()}
Row(
modifier = Modifier
.height(rowHeight)
.border(width = lineThickness,
color = Color.Black,
shape = getBottomLineShape(lineThickness))
) {
// Stuff in the row
}
Using a "Divider" worked for me,
Column {
Divider (
color = Color.White,
modifier = Modifier
.height(1.dp)
.fillMaxHeight()
.fillMaxWidth()
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
) {
// Something else
}
}