Hi all, I am trying to implement a border around ...
# compose
b
Hi all, I am trying to implement a border around focused items, I have tried several approaches but most of them makes my component jump a bit and the other solution makes the border overlaps a bit with the content of the item and block them like checkboxes. Is there any ideas where can this be achieved in an easier / more generic way in compose
1
k
use a Popup and absolute coordinates
s
Would it be possible to do it using a Canvas which goes outside of the content's normal bounds?
a
@Badran what do you mean by 'jumps' a bit? do you mean that adding a border affects the layout and as a result moves the component?
b
Thanks all, I'll have a look at the Popup too I wanted to achieve it initially by only using modifier ext, but that doesn't seem to give the best experience when the app is already built otherwise many changes will be required to keep the design the same
@Alex Styl 100% due to padding when focus border appear the element will resize and looks weird 😓
a
borders do affect layout. not just in compose. it's better to use an outline that does not (which is what @Stylianos Gakis is suggesting). here is what I use and works great in my apps:
Copy code
// Outline.kt

import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

fun Modifier.outline(thickness: Dp, color: Color, shape: Shape = RectangleShape, offset: Dp = 0.dp): Modifier {
    return this.drawBehind {
        val strokeWidth = thickness.toPx()
        val outline = shape.createOutline(size, layoutDirection, this)

        when (outline) {
            is Outline.Generic -> {
                // not supported
            }

            is Outline.Rectangle -> {
                val inset = offset.toPx()

                val path = Path().apply {
                    addRect(
                        Rect(
                            left = inset - strokeWidth,
                            top = inset - strokeWidth,
                            right = size.width - inset + strokeWidth,
                            bottom = size.height - inset + strokeWidth
                        )
                    )
                    fillType = PathFillType.EvenOdd
                    addRect(
                        Rect(
                            left = inset,
                            top = inset,
                            right = size.width - inset,
                            bottom = size.height - inset
                        )
                    )
                }

                drawPath(path, color)
            }

            is Outline.Rounded -> {
                val inset = offset.toPx()
                val roundRect = outline.roundRect

                val topLeftRadius = roundRect.topLeftCornerRadius.x
                val topRightRadius = roundRect.topRightCornerRadius.x
                val bottomRightRadius = roundRect.bottomRightCornerRadius.x
                val bottomLeftRadius = roundRect.bottomLeftCornerRadius.y

                val topLeftOutlineRadius = topLeftRadius + strokeWidth
                val topRightOutlineRadius = topRightRadius + strokeWidth
                val bottomRightOutlineRadius = bottomRightRadius + strokeWidth
                val bottomLeftOutlineRadius = bottomLeftRadius + strokeWidth

                val path = Path().apply {
                    addRoundRect(
                        RoundRect(
                            left = inset - strokeWidth,
                            top = inset - strokeWidth,
                            right = size.width - inset + strokeWidth,
                            bottom = size.height - inset + strokeWidth,
                            topLeftCornerRadius = CornerRadius(topLeftOutlineRadius, topLeftOutlineRadius),
                            topRightCornerRadius = CornerRadius(topRightOutlineRadius, topRightOutlineRadius),
                            bottomRightCornerRadius = CornerRadius(bottomRightOutlineRadius, bottomRightOutlineRadius),
                            bottomLeftCornerRadius = CornerRadius(bottomLeftOutlineRadius, bottomLeftOutlineRadius)
                        )
                    )
                    fillType = PathFillType.EvenOdd
                    addRoundRect(
                        RoundRect(
                            left = inset,
                            top = inset,
                            right = size.width - inset,
                            bottom = size.height - inset,
                            topLeftCornerRadius = CornerRadius(topLeftRadius, topLeftRadius),
                            topRightCornerRadius = CornerRadius(topRightRadius, topRightRadius),
                            bottomRightCornerRadius = CornerRadius(bottomRightRadius, bottomRightRadius),
                            bottomLeftCornerRadius = CornerRadius(bottomLeftRadius, bottomLeftRadius)
                        )
                    )
                }

                drawPath(path, color)
            }
        }
    }
}
☝️ 1
and for focus related one:
Copy code
// FocusRing.kt
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
import com.composeunstyled.theme.Theme

@Composable
fun Modifier.focusRing(interactionSource: InteractionSource, shape: Shape): Modifier {
    val focused by interactionSource.collectIsFocusedAsState()
    val focusRingColor = if (focused) Theme[colors][focusRing] else Color.Transparent
    return this.outline(2.dp, focusRingColor, shape).clip(shape)
}
shape is the shape of the item that needs to be outlined. it is not the shape of the outline itself
b
makes sense, thanks all. drawBehind is it 😄 !