Android - Jetpack Compose draw text with centered lines on sides - android

I am trying to make this simple view in Compose, but cant seem to get it right.
I have tried using dividers, and now switched to canvas, but cant seem to get correct result.
Row{
Line()
ClickableText(text = AnnotatedString("Show replies"),
modifier = Modifier.weight(1f),
onClick = { showReplies.value = true })
Line()
}
#Composable
fun RowScope.Line() {
Canvas(modifier = Modifier.fillMaxSize().weight(1f)) {
// Fetching width and height for
// setting start x and end y
val canvasWidth = size.width
val canvasHeight = size.height
// drawing a line between start(x,y) and end(x,y)
drawLine(
start = Offset(x = 0f, y = canvasHeight/2),
end = Offset(x = canvasWidth, y = canvasHeight/2),
color = Color.Red,
strokeWidth = 5F
)
}
}
I have played with arragments, weights, sizes, but always get some quirky result.

Try this:
#Composable
fun Replies() {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier
.height(2.dp)
.weight(1f)
.background(Color.Gray)) {}
ClickableText(
text = AnnotatedString("Show replies"), onClick = {}, modifier = Modifier.weight(1f),
style = TextStyle(
textAlign = TextAlign.Center
),
)
Box(modifier = Modifier
.height(2.dp)
.weight(1f)
.background(Color.Gray)) {}
}
}

Related

How to draw this easily on compose?

if anyone can tell me how to draw this shape
with an inside text, id greatly appreciate it.
Is there a way to do it in regular xml or any android api
You can use the Canvas to draw any shapes. Below is a sample implementation as per your required shape which you can customize.
Box {
Canvas(
modifier = Modifier
.size(200.dp)
.padding(40.dp)
) {
val trianglePath = Path().let {
it.moveTo(this.size.width * .40f, 0f)
it.lineTo(this.size.width * .50f, -30f)
it.lineTo(this.size.width * .60f, 0f)
it.close()
it
}
drawRoundRect(
Color.LightGray,
size = Size(this.size.width, this.size.height * 0.95f),
cornerRadius = CornerRadius(60f)
)
drawPath(
path = trianglePath,
Color.LightGray,
)
}
}
Easiest way you can do it with shadow and border properties is to create a custom Shape.
If you draw to Canvas alone you will also need to draw shadow and border properties, and need to create offsets inside your Composables bounds.
With GenericShape you can create a Path with roundedRect and add a triangle to top with
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 ->
this.addRoundRect(
RoundRect(
rect = Rect(
offset = Offset(0f, arrowHeightPx),
size = Size(size.width, size.height - arrowHeightPx)
),
cornerRadius = CornerRadius(cornerRadiusPx, cornerRadiusPx)
)
)
moveTo(arrowOffsetPx, arrowHeightPx)
lineTo(arrowOffsetPx + arrowWidthPx / 2, 0f)
lineTo(arrowOffsetPx + arrowWidthPx, arrowHeightPx)
}
}
If you need to have borders instead of addRoundRect you will need arcs and lines to draw for connecting arrow and lines you can refer this answer for drawing rounded rectangle using arcs and lines. If you set border with current setup you will understand what i mean
Then use it as
#Composable
private fun BubbleShapeSample() {
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
)
}
Column(
modifier = Modifier
.shadow(5.dp, bubbleShape)
.background(Color.White)
.padding(8.dp)
) {
Spacer(modifier = Modifier.height(arrowHeight))
Row(modifier = Modifier.padding(12.dp)) {
Icon(
modifier = Modifier.size(60.dp),
imageVector = Icons.Default.NotificationsActive,
contentDescription = "",
tint = Color(0xffFFC107)
)
Spacer(modifier = Modifier.width(20.dp))
Text(
"Get updates\n" +
"on questions\n" +
"and answers",
fontSize = 20.sp
)
Spacer(modifier = Modifier.width(20.dp))
Icon(
imageVector = Icons.Default.Close,
contentDescription = ""
)
}
}
}

Jetpack Compose Arc/Circular Progress Bar Animation (How to restart animation)

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 = ""
)
}
}

How to draw circle shape border stroke drawn with separated lines?

I'm trying to achieve this custom shape with Compose
But for some reason the separator offseted circle is drawn with a dotted line and here is the code
#Preview
#Composable
private fun ReceiptSeparator () {
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp) ,
verticalAlignment = Alignment.CenterVertically ,) {
Box(
modifier = Modifier
.requiredSize(50.dp)
.background(Color.White)
.offset(-40.dp)
.clip(CircleShape)
.border(BorderStroke(2.dp, Color.Gray))
){}
Box(
Modifier
.height(1.dp)
.requiredWidth(250.dp)
.weight(3f)
.background(Color.Gray, shape = DottedShape(step = 20.dp))
){}
Box(
modifier = Modifier
.offset(40.dp)
.clip(CircleShape)
.border(BorderStroke(2.dp, Color.Gray))
.background(Color.White)
.size(50.dp)
){}
}
}
Why the circle is drawn with a dotted line and how to achieve this shape correctly?
Your circle is not drawn correctly, because Modifier.border draws a rectangle border by default, and then you clip it with your Modifier.clip. Instead, if you need to apply shape to the border, you need to pass the shape into Modifier.border, like this:
.border(BorderStroke(2.dp, Color.Gray), shape = CircleShape)
But this won't solve your problem. To draw the shadow correctly like shown in your image, you need to apply a custom Shape to your container.
You can use Modifier.onGloballyPositioned to get position of your cutoffs:
var separatorOffsetY by remember { mutableStateOf<Float?>(null) }
val cornerRadius = 20.dp
Card(
shape = RoundedCutoutShape(separatorOffsetY, cornerRadius),
backgroundColor = Color.White,
modifier = Modifier.padding(10.dp)
) {
Column {
Box(modifier = Modifier.height(200.dp))
Box(
Modifier
.padding(horizontal = cornerRadius)
.height(1.dp)
.requiredWidth(250.dp)
// DottedShape is taken from this answer:
// https://stackoverflow.com/a/68789205/3585796
.background(Color.Gray, shape = DottedShape(step = 20.dp))
.onGloballyPositioned {
separatorOffsetY = it.boundsInParent().center.y
}
)
Box(modifier = Modifier.height(50.dp))
}
}
Using this information you can create a shape like following:
class RoundedCutoutShape(
private val offsetY: Float?,
private val cornerRadiusDp: Dp,
) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density,
) = Outline.Generic(run path#{
val cornerRadius = with(density) { cornerRadiusDp.toPx() }
val rect = Rect(Offset.Zero, size)
val mainPath = Path().apply {
addRoundRect(RoundRect(rect, CornerRadius(cornerRadius)))
}
if (offsetY == null) return#path mainPath
val cutoutPath = Path().apply {
val circleSize = Size(cornerRadius, cornerRadius) * 2f
val visiblePart = 0.25f
val leftOval = Rect(
offset = Offset(
x = 0 - circleSize.width * (1 - visiblePart),
y = offsetY - circleSize.height / 2
),
size = circleSize
)
val rightOval = Rect(
offset = Offset(
x = rect.width - circleSize.width * visiblePart,
y = offsetY - circleSize.height / 2
),
size = circleSize
)
addOval(leftOval)
addOval(rightOval)
}
return#path Path().apply {
op(mainPath, cutoutPath, PathOperation.Difference)
}
})
}
Result:
Get rid of:
shape = DottedShape(step = 20.dp)

how to add border on bottom only in jetpack compose

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
}
}

Construct Rings using Jetpack Compose

I am learning Jetpack Compose and would like to build something like this
I have tried using Box layout by stacking CircularProgressIndicator but requires hardcoding the circle sizes. I want the rings to be size agnostic.
How do I achieve this using Compose?
You can try to do with Canvas. I did this and could give you a start point to achieve what you want...
#Composable
fun DrawGradientCircles() {
Canvas(
modifier = Modifier
.size(300.dp)
.background(Color.Gray)
) {
drawCircle(
brush = Brush.sweepGradient(listOf(Color.Magenta, Color.Red)),
radius = 300f,
style = Stroke(90f)
)
drawCircle(
brush = Brush.sweepGradient(listOf(Color.Green, Color.Yellow)),
radius = 200f,
style = Stroke(90f)
)
drawCircle(
brush = Brush.sweepGradient(listOf(Color.Cyan, Color.Blue)),
radius = 100f,
style = Stroke(90f)
)
}
}
This is the result:
EDIT: I posted an updated version here:
https://gist.github.com/nglauber/e947dacf50155fb72408e83f6595e430
Hope it helps.
I was able to accomplish it using CircularProgressIndicator
#Composable
fun ringView(){
var sz by remember { mutableStateOf(Size.Zero)}
Box(
Modifier
.aspectRatio(1f)
.fillMaxSize()
.background(Color.Blue)
.onGloballyPositioned { coordinates ->
sz = coordinates.size.toSize()
}
, contentAlignment = Alignment.Center){
Box(Modifier.aspectRatio(1f), contentAlignment = Alignment.Center){
Text(text = pxToDp(sz.height.toInt()).toString())
CircularProgressIndicator(progress = 0.9F, Modifier.size(pxToDp(sz.width.toInt()).dp), strokeWidth = (pxToDp(sz.width.toInt())/15).dp,color = Color.Green)
CircularProgressIndicator(progress = 0.9F, Modifier.size((pxToDp(sz.width.toInt())-(2*(pxToDp(sz.width.toInt())/15))).dp), strokeWidth = (pxToDp(sz.width.toInt())/15).dp, color = Color.Black )
CircularProgressIndicator(progress = 0.9F, Modifier.size((pxToDp(sz.width.toInt())-(4*(pxToDp(sz.width.toInt())/15))).dp), strokeWidth = (pxToDp(sz.width.toInt())/15).dp, color = Color.Gray )
CircularProgressIndicator(progress = 0.9F, Modifier.size((pxToDp(sz.width.toInt())-(6*(pxToDp(sz.width.toInt())/15))).dp), strokeWidth = (pxToDp(sz.width.toInt())/15).dp, color = Color.Cyan )
CircularProgressIndicator(progress = 0.9F, Modifier.size((pxToDp(sz.width.toInt())-(8*(pxToDp(sz.width.toInt())/15))).dp), strokeWidth = (pxToDp(sz.width.toInt())/15).dp, color = Color.Magenta )
}
}
}
fun pxToDp(px: Int): Int {
return (px / Resources.getSystem().displayMetrics.density).toInt()
}

Categories

Resources