How can I create new Modifiers that depend on some other Modifiers' values? Say, for the sake of an example, I want to make the child components's height to be half of the one that is passed in from the parent plus some constant (thus not being able to use fillMaxHeight(fraction: Float)).
#Composable
fun View() {
MyComposable(modifier = Modifier.size(40.dp))
}
#Composable
fun MyComposable(
modifier: Modifier // Assuming this modifier contains a size
) {
Box(modifier) { // Setting the the size from the passed in modifier
val childHeightModifier = Modifier.height(???) // <-- Wanted to be some specific value depending on parent size
Box(childHeightModifier) {
...
}
}
}
You can use BoxWithConstraints for this particular usecase, it provides constraints imposed by parent to its children:
#Composable
fun View() {
MyComposable(modifier = Modifier.size(40.dp))
}
#Composable
fun MyComposable(modifier: Modifier) {
BoxWithConstraints(modifier) { // this: BoxWithConstraintsScope
val childHeightModifier = Modifier.height(maxHeight * 0.5f)
Box(childHeightModifier) {
...
}
}
}
As for communication between Modifiers, there's a ModifierLocal system in the works, which will allow doing exactly that.
Related
So I want to get the compose height/width before it's drawn into UI screen, does anyone know if it's possible to do that? And if yes, how?
I tried Modifier.onPlaced { } but I think it's already drawn into the UI before I do any modification with that information
If you provide code i might be able to provide more specific sample of SubcomposeLayout but i use it to measure dimensions of a Composable without triggering recomposition and when BoxWithConstraints is not enough. As in link in comment under some conditions maxWidth or height does not match composable dimensions.
I use it subcompose some section of Composables to pass required dimensions or like building a Slider with dynamic thumb size, chat rows, and so on. I posted several answer about SubcomposeLayout. You can check out this link to see some samples
The recent one i use is a sample one with single main and dependent Composables as in this link
#Composable
internal fun DimensionSubcomposeLayout(
modifier: Modifier = Modifier,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (DpSize) -> Unit
) {
val density = LocalDensity.current
SubcomposeLayout(
modifier = modifier
) { constraints: Constraints ->
// Subcompose(compose only a section) main content and get Placeable
val mainPlaceable: Placeable = subcompose(SlotsEnum.Main, mainContent)
.map {
it.measure(constraints.copy(minWidth = 0, minHeight = 0))
}.first()
val dependentPlaceable: Placeable =
subcompose(SlotsEnum.Dependent) {
dependentContent(
DpSize(
density.run { mainPlaceable.width.toDp() },
density.run { mainPlaceable.height.toDp() }
)
)
}
.map { measurable: Measurable ->
measurable.measure(constraints)
}.first()
layout(mainPlaceable.width, mainPlaceable.height) {
dependentPlaceable.placeRelative(0, 0)
}
}
}
/**
* Enum class for SubcomposeLayouts with main and dependent Composables
*/
enum class SlotsEnum { Main, Dependent }
Passing a Composable as mainContent param will return its size in Dp without recomposition for dependentContent.
I need size of my composable to draw dynamic lines but I don't want to get size by:
var size by remember { mutableStateOf(IntSize.Zero) }
Modifier.onSizeChanged{size = it}
or
Modifier.onGloballyPositioned{size = it.size}
because I don't want to recompose.
Currently I am getting size from BoxWithConstraints and passing as parameter like this:
fun DrawLines(intSize:IntSize){
// handle lines
}
Is there any better approach or that's all I can do for now?
Thanks for help.
If you are gonna use size of your Composable for drawing lines you can change your function to extension of DrawScope which returns size of your Composable. If that's not the case check answer below.
fun DrawScope.drawLine() {
this.size
}
And call this function inside either of
Modifier.drawBehind{}, Modifier.drawWithContent{} or Modifier.drawWithCache{}. Also you can pass size inside these Modifiers if you don't want to change your function.
BoxWithConstraints and SubcomposeLayout
BoxWithConstraints is not always reliable to get exact size of your content, it, as the name suggests, is good for getting Constraints. I have a detailed answer about Constraints and what BoxConstraints return with which size modifier here. You can check out Constraints section of answer to examine outcomes with each Modifier.
For instance
BoxWithConstraints() {
Text(
modifier = Modifier
.size(200.dp)
.border(2.dp, Color.Red),
text = "Constraints: ${constraints.minWidth}, max: ${constraints.maxWidth}"
)
}
Will return minWidth = 0px, and maxWidth 1080px(width of my device in px) instead of 525px which is 200.dp in my device.
And you can't get dimensions from Layout alone without recomposing either that's why BoxWithConstraints uses SubcomposeLayout to pass Constraints to content. You can check out this question to learn about SubcomposeLayout.
BoxWithConstraints source code
#Composable
#UiComposable
fun BoxWithConstraints(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content:
#Composable #UiComposable BoxWithConstraintsScope.() -> Unit
) {
val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
SubcomposeLayout(modifier) { constraints ->
val scope = BoxWithConstraintsScopeImpl(this, constraints)
val measurables = subcompose(Unit) { scope.content() }
with(measurePolicy) { measure(measurables, constraints) }
}
}
SubcomposeLayout allows deferring the composition and measure of
content until its constraints from its parent are known and some its
content can be measured, the results from which and can be passed as a
parameter to the deferred content.
In the implementation below, it can be customized as required, i use several versions of it based on why it's needed.
You can customize how you sum or max width or height, what will be
layout width or height, how you place your items to have a behavior like Row, Column or Box depends on your needs. You can limit to one Composable or multiple ones is up to you. Only thing that is required is passing Size/IntSize/DpSize from one Composable to another.
/**
* SubcomposeLayout that [SubcomposeMeasureScope.subcompose]s [mainContent]
* and gets total size of [mainContent] and passes this size to [dependentContent].
* This layout passes exact size of content unlike
* BoxWithConstraints which returns [Constraints] that doesn't match Composable dimensions under
* some circumstances
*
* #param placeMainContent when set to true places main content. Set this flag to false
* when dimensions of content is required for inside [mainContent]. Just measure it then pass
* its dimensions to any child composable
*
* #param mainContent Composable is used for calculating size and pass it
* to Composables that depend on it
*
* #param dependentContent Composable requires dimensions of [mainContent] to set its size.
* One example for this is overlay over Composable that should match [mainContent] size.
*
*/
#Composable
fun DimensionSubcomposeLayout(
modifier: Modifier = Modifier,
placeMainContent: Boolean = true,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (Size) -> Unit
) {
SubcomposeLayout(
modifier = modifier
) { constraints: Constraints ->
// Subcompose(compose only a section) main content and get Placeable
val mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent)
.map {
it.measure(constraints.copy(minWidth = 0, minHeight = 0))
}
// Get max width and height of main component
var maxWidth = 0
var maxHeight = 0
mainPlaceables.forEach { placeable: Placeable ->
maxWidth += placeable.width
maxHeight = placeable.height
}
val dependentPlaceables: List<Placeable> = subcompose(SlotsEnum.Dependent) {
dependentContent(Size(maxWidth.toFloat(), maxHeight.toFloat()))
}
.map { measurable: Measurable ->
measurable.measure(constraints)
}
layout(maxWidth, maxHeight) {
if (placeMainContent) {
mainPlaceables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
dependentPlaceables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
}
}
enum class SlotsEnum { Main, Dependent }
Usage
val content = #Composable {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Red)
)
}
val density = LocalDensity.current
DimensionSubcomposeLayout(
mainContent = { content() },
dependentContent = { size: Size ->
content()
val dpSize = density.run {size.toDpSize() }
Box(Modifier.size(dpSize).border(3.dp, Color.Green))
},
placeMainContent = false
)
or
DimensionSubcomposeLayout(
mainContent = { content() },
dependentContent = { size: Size ->
val dpSize = density.run {size.toDpSize() }
Box(Modifier.size(dpSize).border(3.dp, Color.Green))
}
)
Result
In example below we set size of Box with green border based on Box with red background. This can be complicated for a beginner but that's how you get dimensions without recomposing a Composable. SubcomposeLayout question and answers in the link provided above might help. I posted several answers and linked other answers show how to use it.
Extra Section
Layouts, Scopes, and Constraining Siblings
You can use layout in similar way Box, Row, Column does with a scope to pass information from inside to content using an interface, implementation and changing properties of this implementation
interface DimensionScope {
var size: Size
}
class DimensionScopeImpl(override var size: Size = Size.Zero) : DimensionScope
And implementing DimensionScope and Layout.
#Composable
private fun DimensionLayout(
modifier: Modifier = Modifier,
content: #Composable DimensionScope.() -> Unit
) {
val dimensionScope = remember{DimensionScopeImpl()}
Layout(
modifier = modifier,
// 🔥 since we invoke it here it will have Size.Zero
// on Composition then will have size value below
content = { dimensionScope.content() }
) { measurables: List<Measurable>, constraints: Constraints ->
val placeables = measurables.map { measurable: Measurable ->
measurable.measure(constraints)
}
val maxWidth = placeables.maxOf { it.width }
val maxHeight = placeables.maxOf { it.height }
dimensionScope.size = Size(maxWidth.toFloat(), maxHeight.toFloat())
layout(maxWidth, maxHeight) {
placeables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
}
}
Since we invoke before being able to measure, and with Layout we can only measure once, we won't be able to pass correct Size to DimensionScopeImpl on first composition as i mentioned above. On next recompositions since we remember DimensionScopeImpl we get the correct size and Text size is correctly set and we see Text with border.
Column(modifier = Modifier.fillMaxSize().padding(20.dp)) {
val density = LocalDensity.current
var counter by remember { mutableStateOf(0) }
DimensionLayout {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Red)
)
val dpSize = density.run { size.toDpSize() }
Text(
text = "counter: $counter", modifier = Modifier
.size(dpSize)
.border(3.dp, Color.Green)
)
}
Button(onClick = { counter++ }) {
Text("Counter")
}
}
We are not able to get correct size because we needed to invoke dimensionScope.content() before measuring but in some cases you might be able to get Constraints, size or parameters from parent or your calculation. When that's the case you can pass you Size. I made an image that passes drawing area based on ContentScale as you can see here using scope.
Selectively measuring to match one Sibling to Another
Not being able to pass using Layout doesn't mean we can't set other sibling to same size and use its dimensions if needed.
For demonstration we will change dimensions of second Composable to firs one's
#Composable
private fun MatchDimensionsLayout(
modifier: Modifier = Modifier,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables: List<Measurable>, constraints: Constraints ->
// For demonstration we will change dimensions of second Composable to firs ones
require(measurables.size == 2)
val firstMeasurable = measurables.first()
val secondMeasurable = measurables.last()
val firsPlaceable = firstMeasurable.measure(constraints)
// Measure with first one's width and height
val secondPlaceable =
secondMeasurable.measure(Constraints.fixed(firsPlaceable.width, firsPlaceable.height))
// Set width and height of this Composable width of first one, height total of first
// and second
val containerWidth = firsPlaceable.width
val containerHeight = firsPlaceable.height + secondPlaceable.height
layout(containerWidth, containerHeight) {
firsPlaceable.placeRelative(0,0)
val y = firsPlaceable.height
secondPlaceable.placeRelative(0,y)
}
}
}
Demonstration
MatchDimensionsLayout {
BoxWithConstraints {
Text(
modifier = Modifier
.size(200.dp)
.border(2.dp, Color.Red),
text = "Constraints: ${constraints.minWidth}\n" +
"max: ${constraints.maxWidth}"
)
}
BoxWithConstraints {
Text(
modifier = Modifier
.size(400.dp)
.border(2.dp, Color.Red),
text = "Constraints: ${constraints.minWidth}\n" +
"max: ${constraints.maxWidth}"
)
}
}
Since we matched size of second one to first one using Constraints.fixed for measuring BoxWithConstraints now returns dimensions of first or main Composable even if we are not able to pass dimensions from Layout as parameters.
You can also use Modifier.layoutId() instead of first or second to select Composable that you need to use as reference for measuring others
Here is my problem;
When I add MyText composable in my Screen, I see all Logs (value1, value2, value3) which means it is recomposing every part of my code.
However when I comment the MyText line, I see only value3 on Logcat
How can I fix this ? I know it is not a big problem here but just imagine we have a scrollable Column here and we are trying to pass ScrollState.value to My Text component. Because of this situation, our list becomes so laggy.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Screen()
}
}
}
#Composable
fun Screen(){
var counter by remember {
mutableStateOf(0)
}
Log.i("RECOMPOSE","VALUE1")
Column() {
Text(text = "Just a text")
Log.i("RECOMPOSE","VALUE2")
Button(onClick = { counter = counter.plus(1) }) {
Text(text = counter.toString())
Log.i("RECOMPOSE","VALUE3")
}
MyText(counter)
}
}
#Composable
fun MyText(counter:Int){
Text(text = counter.toString())
}
EDIT
There is main problem, with Scrollable Column;
#Composable
fun Screen(){
val scrollState = rememberScrollState()
Box() {
Column(modifier = Modifier
.verticalScroll(scrollState)
.padding(top = 50.dp)) {
//Some Static Column Elements with images etc.
}
MyText(scrollStateValue = scrollState.value) //Doing some UI staff in this component
}
}
#Composable
fun MyText(scrollStateValue:Int){
Text(text = scrollStateValue.toString())
}
This behaviour is totally expected.
Compose is trying to reduce number of recompositions as much as possible. When you comment out MyText, the only view that depends on counter is Button content, so this is the only view that needs to be recomposed.
By the same logic you shouldn't see VALUE1 logs more than once, but the difference here is that Column is inline function, so if it content needs to be recomposed - it gets recomposed with the containing view.
Using this knowledge, you can easily prevent a view from being recomposed: you need to move part, which doesn't depends on the state, into a separate composable. The fact that it uses scrollState won't make it recompose, only reading state value will trigger recomposition.
#Composable
fun Screen(){
val scrollState = rememberScrollState()
Box() {
YourColumn(scrollState)
MyText(scrollStateValue = scrollState.value) //Doing some UI staff in this component
}
}
#Composable
fun YourColumn(scrollState: ScrollState){
Column(modifier = Modifier
.verticalScroll(scrollState)
.padding(top = 50.dp)) {
//Some Static Column Elements with images etc.
}
}
As can be seen in official documents there is layout named SubcomposeLayout defined as
Analogue of Layout which allows to subcompose the actual content
during the measuring stage for example to use the values calculated
during the measurement as params for the composition of the children.
Possible use cases:
You need to know the constraints passed by the parent during the
composition and can't solve your use case with just custom Layout or
LayoutModifier. See
androidx.compose.foundation.layout.BoxWithConstraints.
You want to use the size of one child during the composition of the
second child.
You want to compose your items lazily based on the available size. For
example you have a list of 100 items and instead of composing all of
them you only compose the ones which are currently visible(say 5 of
them) and compose next items when the component is scrolled.
I searched Stackoverflow with SubcomposeLayout keyword but couldn't find anything about it, created this sample code, copied most of it from official document, to test and learn how it works
#Composable
private fun SampleContent() {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
SubComponent(
mainContent = {
Text(
"MainContent",
modifier = Modifier
.background(Color(0xffF44336))
.height(60.dp),
color = Color.White
)
},
dependentContent = {
val size = it
println("🤔 Dependent size: $size")
Column() {
Text(
"Dependent Content",
modifier = Modifier
.background(Color(0xff9C27B0)),
color = Color.White
)
}
}
)
}
}
#Composable
private fun SubComponent(
mainContent: #Composable () -> Unit,
dependentContent: #Composable (IntSize) -> Unit
) {
SubcomposeLayout { constraints ->
val mainPlaceables = subcompose(SlotsEnum.Main, mainContent).map {
it.measure(constraints)
}
val maxSize = mainPlaceables.fold(IntSize.Zero) { currentMax, placeable ->
IntSize(
width = maxOf(currentMax.width, placeable.width),
height = maxOf(currentMax.height, placeable.height)
)
}
layout(maxSize.width, maxSize.height) {
mainPlaceables.forEach { it.placeRelative(0, 0) }
subcompose(SlotsEnum.Dependent) {
dependentContent(maxSize)
}.forEach {
it.measure(constraints).placeRelative(0, 0)
}
}
}
}
enum class SlotsEnum { Main, Dependent }
It's supposed to re-measure a component based on another component size but what this code actually does is a mystery to me.
How does subcompose function work?
What's the point of slotId and can we get slotId in a way?
The description for subCompose function
Performs subcomposition of the provided content with given slotId.
Params: slotId - unique id which represents the slot we are composing
into. If you have fixed amount or slots you can use enums as slot ids,
or if you have a list of items maybe an index in the list or some
other unique key can work. To be able to correctly match the content
between remeasures you should provide the object which is equals to
the one you used during the previous measuring. content - the
composable content which defines the slot. It could emit multiple
layouts, in this case the returned list of Measurables will have
multiple elements.
Can someone explain what it means or/and provide a working sample for SubcomposeLayout?
It's supposed to re-measure a component based on another component size...
SubcomposeLayout doesn't remeasure. It allows deferring the composition and measure of content until its constraints from its parent are known and some its content can be measured, the results from which and can be passed as a parameter to the deferred content. The above example calculates the maximum size of the content generated by mainContent and passes it as a parameter to deferredContent. It then measures deferredContent and places both mainContent and deferredContent on top of each other.
The simplest example of how to use SubcomposeLayout is BoxWithConstraints that just passes the constraints it receives from its parent directly to its content. The constraints of the box are not known until the siblings of the box have been measured by the parent which occurs during layout so the composition of content is deferred until layout.
Similarly, for the example above, the maxSize of mainContent is not known until layout so deferredContent is called in layout once maxSize is calculated. It always places deferredContent on top of mainContent so it is assumed that deferredContent uses maxSize in some way to avoid obscuring the content generated by mainContent. Probably not the best design for a composable but the composable was intended to be illustrative not useful itself.
Note that subcompose can be called multiple times in the layout block. This is, for example, what happens in LazyRow. The slotId allows SubcomposeLayout to track and manage the compositions created by calling subcompose. For example, if you are generating the content from an array you might want use the index of the array as its slotId allowing SubcomposeLayout to determine which subcompose generated last time should be used to during recomposition. Also, if a slotid is not used any more, SubcomposeLayout will dispose its corresponding composition.
As for where the slotId goes, that is up to the caller of SubcomposeLayout. If the content needs it, pass it as a parameter. The above example doesn't need it as the slotId is always the same for deferredContent so it doesn't need to go anywhere.
I made a sample based on the sample provided by official documents and #chuckj's answer but still not sure if this efficient or right way to implement it.
It basically measures longest component sets parent width and remeasures shorter one with minimumWidth of Constraint and resizes short one as can be seen in this gif. This is how whatsapp scales quote and message length basically.
Orange and pink containers are Columns, which direct children of DynamicWidthLayout, that uses SubcomposeLayout to remeasure.
#Composable
private fun DynamicWidthLayout(
modifier: Modifier = Modifier,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (IntSize) -> Unit
) {
SubcomposeLayout(modifier = modifier) { constraints ->
var mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent).map {
it.measure(constraints)
}
var maxSize =
mainPlaceables.fold(IntSize.Zero) { currentMax: IntSize, placeable: Placeable ->
IntSize(
width = maxOf(currentMax.width, placeable.width),
height = maxOf(currentMax.height, placeable.height)
)
}
val dependentMeasurables: List<Measurable> = subcompose(SlotsEnum.Dependent) {
// 🔥🔥 Send maxSize of mainComponent to
// dependent composable in case it might be used
dependentContent(maxSize)
}
val dependentPlaceables: List<Placeable> = dependentMeasurables
.map { measurable: Measurable ->
measurable.measure(Constraints(maxSize.width, constraints.maxWidth))
}
// Get maximum width of dependent composable
val maxWidth = dependentPlaceables.maxOf { it.width }
println("🔥 DynamicWidthLayout-> maxSize width: ${maxSize.width}, height: ${maxSize.height}")
// If width of dependent composable is longer than main one, remeasure main one
// with dependent composable's width using it as minimumWidthConstraint
if (maxWidth > maxSize.width) {
println("🚀 DynamicWidthLayout REMEASURE MAIN COMPONENT")
// !!! 🔥🤔 CANNOT use SlotsEnum.Main here why?
mainPlaceables = subcompose(2, mainContent).map {
it.measure(Constraints(maxWidth, constraints.maxWidth))
}
}
// Our final maxSize is longest width and total height of main and dependent composables
maxSize = IntSize(
maxSize.width.coerceAtLeast(maxWidth),
maxSize.height + dependentPlaceables.maxOf { it.height }
)
layout(maxSize.width, maxSize.height) {
// Place layouts
mainPlaceables.forEach { it.placeRelative(0, 0) }
dependentPlaceables.forEach {
it.placeRelative(0, mainPlaceables.maxOf { it.height })
}
}
}
}
enum class SlotsEnum { Main, Dependent }
Usage
#Composable
private fun TutorialContent() {
val density = LocalDensity.current.density
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
var mainText by remember { mutableStateOf(TextFieldValue("Main Component")) }
var dependentText by remember { mutableStateOf(TextFieldValue("Dependent Component")) }
OutlinedTextField(
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth(),
value = mainText,
label = { Text("Main") },
placeholder = { Text("Set text to change main width") },
onValueChange = { newValue: TextFieldValue ->
mainText = newValue
}
)
OutlinedTextField(
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth(),
value = dependentText,
label = { Text("Dependent") },
placeholder = { Text("Set text to change dependent width") },
onValueChange = { newValue ->
dependentText = newValue
}
)
DynamicWidthLayout(
modifier = Modifier
.padding(8.dp)
.background(Color.LightGray)
.padding(8.dp),
mainContent = {
println("🍏 DynamicWidthLayout-> MainContent {} composed")
Column(
modifier = Modifier
.background(orange400)
.padding(4.dp)
) {
Text(
text = mainText.text,
modifier = Modifier
.background(blue400)
.height(40.dp),
color = Color.White
)
}
},
dependentContent = { size: IntSize ->
// 🔥 Measure max width of main component in dp retrieved
// by subCompose of dependent component from IntSize
val maxWidth = with(density) {
size.width / this
}.dp
println(
"🍎 DynamicWidthLayout-> DependentContent composed " +
"Dependent size: $size, "
+ "maxWidth: $maxWidth"
)
Column(
modifier = Modifier
.background(pink400)
.padding(4.dp)
) {
Text(
text = dependentText.text,
modifier = Modifier
.background(green400),
color = Color.White
)
}
}
)
}
}
And full source code is here.
Recently i needed to use almost the same SubcomposeLayout in question. I needed a Slider with a Composable thumb that i needed to get its width so i can set start and end of track and full width of Slider i was getting from BoxWithConstraints.
enum class SlotsEnum {
Slider, Thumb
}
/**
* [SubcomposeLayout] that measure [thumb] size to set Slider's track start and track width.
* #param thumb thumb Composable
* #param slider Slider composable that contains **thumb** and **track** of this Slider.
*/
#Composable
private fun SliderComposeLayout(
modifier: Modifier = Modifier,
thumb: #Composable () -> Unit,
slider: #Composable (IntSize, Constraints) -> Unit
) {
SubcomposeLayout(modifier = modifier) { constraints: Constraints ->
// Subcompose(compose only a section) main content and get Placeable
val thumbPlaceable: Placeable = subcompose(SlotsEnum.Thumb, thumb).map {
it.measure(constraints)
}.first()
// Width and height of the thumb Composable
val thumbSize = IntSize(thumbPlaceable.width, thumbPlaceable.height)
// Whole Slider Composable
val sliderPlaceable: Placeable = subcompose(SlotsEnum.Slider) {
slider(thumbSize, constraints)
}.map {
it.measure(constraints)
}.first()
val sliderWidth = sliderPlaceable.width
val sliderHeight = sliderPlaceable.height
layout(sliderWidth, sliderHeight) {
sliderPlaceable.placeRelative(0, 0)
}
}
}
Measured thumb and send its dimensions as IntSize and Constraints to Slider, and only placed Slider since thumb is already placed insider Slider, placing here creates two thumbs.
And used it as
SliderComposeLayout(
modifier = modifier
.minimumTouchTargetSize()
.requiredSizeIn(
minWidth = ThumbRadius * 2,
minHeight = ThumbRadius * 2,
),
thumb = { thumb() }
) { thumbSize: IntSize, constraints: Constraints ->
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val width = constraints.maxWidth.toFloat()
val thumbRadiusInPx = (thumbSize.width / 2).toFloat()
// Start of the track used for measuring progress,
// it's line + radius of cap which is half of height of track
// to draw this on canvas starting point of line
// should be at trackStart + trackHeightInPx / 2 while drawing
val trackStart: Float
// End of the track that is used for measuring progress
val trackEnd: Float
val strokeRadius: Float
with(LocalDensity.current) {
strokeRadius = trackHeight.toPx() / 2
trackStart = thumbRadiusInPx.coerceAtLeast(strokeRadius)
trackEnd = width - trackStart
}
// Rest of the code
}
Result
Github link for the code
I want to set a direction of a specific composable to be RTL
#Composable
fun ViewToBeChanged() {
Row {
Image()
Column {
Text("Title")
Text("Subtitle")
}
}
}
Is it possible?
Jetpack compose Layout documentation mentions LocalLayoutDirection
Change the layout direction of a composable by changing the LocalLayoutDirection compositionLocal.
But I have no idea how to use it in a composable to take effect.
You can use the CompositionLocalProvider to provide a custom LocalLayoutDirection.
Something like:
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl ) {
Column(Modifier.fillMaxWidth()) {
Text("Title")
Text("Subtitle")
}
}
Since I did not have your image, I tweaked your composable to:
#Composable
fun ViewToBeChanged() {
Row {
Text("Foo", modifier = Modifier.padding(end = 8.dp))
Column {
Text("Title")
Text("Subtitle")
}
}
}
That gives us:
One way to switch to RTL is to use CompositionLocalProvider and LocalLayoutDirection:
#Composable
fun RtlView() {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
Row {
Text("Foo", modifier = Modifier.padding(end = 8.dp))
Column {
Text("Title")
Text("Subtitle")
}
}
}
}
Here, we are saying that we are overriding the CompositionLocal for layout direction for the contents of the trailing lambda supplied to CompositionLocalProvider(). This gives us:
This changes the layout direction used by this branch of the composable tree, for the composables itself. English is still a LTR language, so the text is unaffected.
As a generalisation of the other answers, if this is needed in different Composables we can define the following
#Composable
fun RightToLeftLayout(content: #Composable () -> Unit) {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
content()
}
}
then simply use
RightToLeftLayout {
ViewToBeChanged()
}
or
RightToLeftLayout {
Row {
...
}
}