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 = ""
)
}
}
Related
This layout is made by me, the layout you are looking is a SVG image so I have just made the image to fill max size and added the above text and camera capture button below. But now I want to remove the image background and want to make the same layout programmatically.
Box(contentAlignment = Alignment.BottomCenter, modifier = Modifier.fillMaxSize()) {
AndroidView({ previewView }, modifier = Modifier.fillMaxSize())
Column(modifier = Modifier.fillMaxSize()) {
Icon(
painter = painterResource(id = R.drawable.ic_card_overlay),
contentDescription = null
)
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.ic_black_transparent),
contentDescription = null,
contentScale = ContentScale.FillWidth
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(26.dp)
) {
Row(
modifier = Modifier
.padding(bottom = 20.dp), verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier.clickable {
onCloseCameraClick()
},
painter = painterResource(id = R.drawable.ic_baseline_arrow_back_ios_24),
contentDescription = null,
tint = Color.White
)
Text(
text = "Passport",
color = Color.White,
fontSize = 20.sp
)
}
Text(
text = "Place your passport inside the frame and take a\npicture.\nMake sure it is not cut or has any glare.",
color = Color.White,
fontSize = 12.sp
)
}
IconButton(
modifier = Modifier.padding(bottom = 20.dp),
onClick = {
Log.d("takePhoto", "ON CLICK")
takePhoto(
imageCapture = imageCapture,
outputDirectory = outputDirectory,
executor = executor,
onImageCaptured = onImageCaptured,
onError = onError
)
},
content = {
Icon(
painter = painterResource(id = R.drawable.ic_baseline_camera_24),
contentDescription = stringResource(R.string.take_picture),
tint = Color.White,
modifier = Modifier
.fillMaxSize(0.2f)
)
}
)
}
You can see I have used ic_card_overlay image which act like a background. I want to achieve the same black transparent background with the box in the middle which will not include the black transparent color. Thank you.
You can achieve this with using BlendMode.Clear
#Composable
fun TransparentClipLayout(
modifier: Modifier,
width: Dp,
height: Dp,
offsetY: Dp
) {
val offsetInPx: Float
val widthInPx: Float
val heightInPx: Float
with(LocalDensity.current) {
offsetInPx = offsetY.toPx()
widthInPx = width.toPx()
heightInPx = height.toPx()
}
Canvas(modifier = modifier) {
val canvasWidth = size.width
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
// Destination
drawRect(Color(0x77000000))
// Source
drawRoundRect(
topLeft = Offset(
x = (canvasWidth - widthInPx) / 2,
y = offsetInPx
),
size = Size(widthInPx, heightInPx),
cornerRadius = CornerRadius(30f,30f),
color = Color.Transparent,
blendMode = BlendMode.Clear
)
restoreToCount(checkPoint)
}
}
}
You can customize corner radius size too. This is only for demonstration
Usage
Column {
Box(modifier = Modifier.fillMaxSize()) {
Image(
modifier =Modifier.fillMaxSize(),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = null,
contentScale = ContentScale.Crop
)
TransparentClipLayout(
modifier = Modifier.fillMaxSize(),
width = 300.dp,
height = 200.dp,
offsetY = 150.dp
)
}
}
Result
You can archieve this background layout using a custom Shape in combination with a Surface. With a custom implementation you can define what parts of the Surface are displayed and which parts are "cut out".
The cutoutPath defines the part which are highlighted. Here it is defined as a RoundRect with a dynamically calculated position and size. Adjust the topLeft and ``formulas as you need.
Using Path.combine(...) the outlinePath is combined with the cutoutPath. This is where the magic happens.
/**
* This is a shape with cuts out a rectangle in the center
*/
class CutOutShape : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
val outlinePath = Path()
outlinePath.addRect(Rect(Offset(0f, 0f), size))
val cutoutHeight = size.height * 0.3f
val cutoutWidth = size.width * 0.75f
val center = Offset(size.width / 2f, size.height / 2f)
val cutoutPath = Path()
cutoutPath.addRoundRect(
RoundRect(
Rect(
topLeft = center - Offset(
cutoutWidth / 2f,
cutoutHeight / 2f
),
bottomRight = center + Offset(
cutoutWidth / 2f,
cutoutHeight / 2f
)
),
cornerRadius = CornerRadius(16f, 16f)
)
)
val finalPath = Path.combine(
PathOperation.Difference,
outlinePath,
cutoutPath
)
return Outline.Generic(finalPath)
}
}
The shape can be used like this:
Surface(
shape = CutOutShape(),
color = Color.Black.copy(alpha = 0.45f)
) { }
This results in the following screen:
Box {
AndroidView({ previewView }, modifier = Modifier.fillMaxSize())
Surface(
shape = CutOutShape(),
color = Color.Black.copy(alpha = 0.45f),
modifier = Modifier.fillMaxSize()
) { }
Column(
modifier = Modifier
.padding(top = 54.dp, start = 32.dp, end = 32.dp, bottom = 54.dp)
) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = { /*TODO*/ }) {
Icon(
imageVector = Icons.TwoTone.ArrowBack,
contentDescription = null,
tint = Color.White
)
}
Text(
"Passport",
color = Color.White,
fontSize = 20.sp
)
}
Text(
"Place your passport inside the frame and take a picture.\nMake sure it is not cut or has any glare.",
color = Color.White,
fontSize = 12.sp
)
Spacer(modifier = Modifier.weight(1f))
Icon(
imageVector = Icons.TwoTone.Camera,
contentDescription = null,
tint = Color.White,
modifier = Modifier
.size(48.dp)
.align(Alignment.CenterHorizontally)
)
}
}
I managed to work this out, and setup 3 cards one on top of the other as seperate boxs compose elements with onclick and on drag properties.
The issue is now, that I'd like the card that I'm pressing/dragging to set to the front, so, I played with the z-index modifier, but, it looks like I'm doing something wrong. Any idea?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Test1Theme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
for (i in 1 until 4) {
DraggableBox(title = "Box_${+1}", initX = 100f*i.toFloat(), initY = 100f, content =
{
Text(text = "Box_${i}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
}
)
}
}
}
}
}
}
#Composable
fun DraggableBox(title: String, initX: Float = 0f, initY: Float = 0f, content: #Composable() () -> Unit) {
val cardInitWidth = 135f
val cardInitHeight = 190f
val expandValue = 20f
Box(
modifier = Modifier
.fillMaxSize()
) {
val shape = RoundedCornerShape(12.dp)
val coroutineScope = rememberCoroutineScope()
val enable = remember { mutableStateOf(true) }
var offsetX = remember { Animatable(initialValue = initX) }
var offsetY = remember { Animatable(initialValue = initY) }
val interactionSource = remember { MutableInteractionSource() }
val clickable = Modifier.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current
) { }
val isPressed by interactionSource.collectIsPressedAsState()
val size = animateSizeAsState(
targetValue = if (enable.value && !isPressed) {
Size(width = cardInitWidth, height = cardInitHeight)
} else {
Size(width = cardInitWidth + expandValue, height = cardInitHeight + expandValue)
}
)
Box(
Modifier
.offset {
IntOffset(
x = offsetX.value.roundToInt(),
y = offsetY.value.roundToInt()
)
}
.zIndex(zIndex = if (enable.value && !isPressed) 5f else 0f)
.size(size.value.width.dp, size.value.height.dp)
.clip(shape)
//.background(Color(0xFF5FA777))
.background(color = MaterialTheme.colors.primary)
.border(BorderStroke(2.dp, Color.Black), shape = shape)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
enable.value = !enable.value
},
onDrag = { change, dragAmount ->
change.consumeAllChanges()
coroutineScope.launch {
offsetX.snapTo(targetValue = offsetX.value + dragAmount.x)
offsetY.snapTo(targetValue = offsetY.value + dragAmount.y)
}
spring(stiffness = Spring.StiffnessHigh, visibilityThreshold = 0.1.dp)
},
onDragEnd = {
enable.value = !enable.value
spring(stiffness = Spring.StiffnessLow, visibilityThreshold = 0.1.dp)
coroutineScope.launch {
launch {
offsetY.animateTo(
targetValue = initY,
animationSpec = tween(
durationMillis = 700,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
}
offsetX.animateTo(
targetValue = initX,
animationSpec = tween(
durationMillis = 700,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
}
}
)
}
.then(clickable)
) {
Row (modifier = Modifier
.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically
)
{
Column (modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
)
{
Column (
horizontalAlignment = Alignment.CenterHorizontally
)
{
Text(text = "init-X: ${initX.toString()}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
Text(text = "init-Y: ${initY.toString()}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
}
Column (
horizontalAlignment = Alignment.CenterHorizontally
)
{
Text(text = "offset-X: ${offsetX.value.roundToInt().toString()}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
Text(text = "offset-Y: ${offsetY.value.roundToInt().toString()}", color = Color.White, fontSize = 16.sp, textAlign = TextAlign.Center)
}
Column (
horizontalAlignment = Alignment.CenterHorizontally
)
{
content()
}
}
}
}
}
}
The Modifier.zIndex works only for children within the same parent.
In your case you should move this modifier to the topmost Box. To do so you have to move enable and isPressed one level up too, and I would move all the other variables as well - but that's just a matter of taste, I guess.
val enable = remember { mutableStateOf(true) }
val isPressed by interactionSource.collectIsPressedAsState()
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(zIndex = if (enable.value && !isPressed) 5f else 0f)
) {
// ...
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
}
}
I have a String "(+91)". How do I animate it such that on some action, "(+91)" fades in gradually, and on some other action "(+91)" fades out gradually. I am using "(+91)" as prefix in my Textfield, with the help of visualTransformation.
This is the code I'm using for my Textfield:
TextField(
value = query3.value,
onValueChange = { newValue ->
query3.value = newValue
mobErrorVisible.value = false
},
visualTransformation = if (showCode){
PrefixTransformation("(+91)")} //Animate (+91)
else
PrefixTransformation(""),
label = {
Text(
"Mobile Number",
color = colorResource(id = R.color.bright_green),
fontFamily = FontFamily(Font(R.font.poppins_regular)),
fontSize = with(LocalDensity.current) { dimensionResource(id = R.dimen._12ssp).toSp() })
},
interactionSource = interactionSource,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
textStyle = TextStyle(
textAlign = TextAlign.Start,
color = colorResource(id = R.color.bright_green),
fontFamily = FontFamily(Font(R.font.poppins_regular)),
fontSize = with(LocalDensity.current) { dimensionResource(id = R.dimen._16ssp).toSp() }
),
modifier = Modifier
.drawBehind {
val strokeWidth = indicatorWidth.value * density
val y = size.height - strokeWidth / 2
drawLine(
indicatorColor,
Offset(TextFieldPadding.toPx(), y),
Offset(size.width - TextFieldPadding.toPx(), y),
strokeWidth
)
}
.focusRequester(focusRequester)
.onFocusChanged { showCode = (it.isFocused || query3.value != "")}
.constrainAs(phone) {
top.linkTo(glPhone)
bottom.linkTo(glPhoneBottom)
start.linkTo(glLeft)
end.linkTo(glRight)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
},
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent,
focusedIndicatorColor = Transparent,
unfocusedIndicatorColor = Transparent,
disabledIndicatorColor = Transparent
)
)
This is my PrefixTransformation class:
class PrefixTransformation(val prefix: String) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
return PrefixFilter(text, prefix)
}
}
This is my PrefixFilter() function:
fun PrefixFilter(number: AnnotatedString, prefix: String): TransformedText {
var out = prefix + " " + number.text
val prefixOffset = prefix.length
val numberOffsetTranslator = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
return offset + prefixOffset
}
override fun transformedToOriginal(offset: Int): Int {
if (offset <= prefixOffset-1) return prefixOffset
return offset - prefixOffset
}
}
return TransformedText(AnnotatedString(out), numberOffsetTranslator)
}
We can animate color as:
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(Modifier.fillMaxSize().background(color.value))
But how do we animate a String?
What you need is the animatedVisibility composable.
Here is an example where the visibility of the text is controlled by the button
Box(
modifier = Modifier
.fillMaxSize()
) {
var visible by remember { mutableStateOf(false) }
Button(
modifier = Modifier.align(Alignment.TopCenter),
onClick = {
visible = !visible
}
) {
Text("Toggle Visibility")
}
val animationDuration = 2000
AnimatedVisibility(
modifier = Modifier.align(Alignment.BottomCenter),
visible = visible,
enter = fadeIn(animationSpec = tween(durationMillis = animationDuration)),
exit = fadeOut(animationSpec = tween(durationMillis = animationDuration))
) {
Text("ABC")
}
}
A sensible solution would be to use a separate Composable for the country code. That way you can wrap the Composable in something like crossfade or just AnimatedVisibility (Experimental as of compose 1.0.0-beta07)