Why is jetpack compose animateDpAsState not working? - android-jetpack-compose

I have a box, and I want to animate its width by using animateDpAsState.
Box(modifier = modifier)
{
val animatedDpState by animateDpAsState(
targetValue = vm.animateWidthTargetState,
tween(3000))
// box with animated width
Box(
modifier = Modifier
.fillMaxHeight()
.width(animatedDpState)
.background(Color.Red))
}
In my viewModel, I have this animateWidthTargetState:
var animateWidthTargetState by mutableStateOf(0.dp)
And somewhere else I changed the value of animateWidthTargetState to that of my screenWidth by clicking a button:
vm.animateWidthTargetState = screenWidth // let's say screenWith = 360.dp
The problem is that the value of animatedDpState becomes 360.dp the instant I clicked on my button, causing the width of my box to become 360.dp instantly. It seemed that no calculation even happened and animateDpAsState was not working.
What should I do to fix it?

Related

Jetpack compose: how to set intrinsic height as minimum, but expand to fill space?

I'm trying to set it so that a TextField refuses to shrink past its intrinsic height (defined by minLines), but also grow to fill max height:
// context: this is inside of a Column that has fillMaxHeight on it
OutlinedTextField(
// unrelated fields
minLines = 26,
modifier = Modifier
//.height(IntrinsicSize.Min)
.fillMaxWidth()
.fillMaxHeight()
.weight(1f)
.height(IntrinsicSize.Max)
)
However, everything I've tried (setting fillMaxHeight, weight(1f), using requiredHeight instead of height, etc) has issues. It seems like it's mutually exclusive: I can either set a minimum height based on intrinsic height or I can grow to fill the height (using weight seems to work for that, but not fillMaxHeight for some reason.
What is the proper way to have IntrinsicSize be the minimum, but grow to fill remaining space?
If i understand this question correctly you want your TextField height to be one line tall then increases as user enters text. You can selectively change modifier using Modifier.then()
Edit
As of jetpack compose 1.4.0-alpha02 minLines param is added to OutlineTextField and other text composables
#Composable
private fun Sample() {
var text by remember {
mutableStateOf("Hello World")
}
var focused by remember {
mutableStateOf(false)
}
Column(modifier = Modifier.fillMaxHeight()) {
OutlinedTextField(
value = text,
onValueChange = {
text = it
},
minLines = 26,
// unrelated fields
modifier = Modifier
.fillMaxWidth()
.then(
if (focused) {
Modifier.wrapContentHeight()
} else Modifier.fillMaxHeight()
)
.onFocusChanged {
focused = it.isFocused
}
)
}
}

How to set the cursor to any part of the text using TextFieldValue (w/ FocusRequester) on pressing/clicking the TextField

I tried to look around but I can't find a way to
force a focus,
set the cursor to the end of text
and still be able to set the cursor to any part of the text when
pressing/clicking.
With FocusRequester the cursor is set to the start of text, but with TextFieldValue(<text>, <range>) I'm able to place the cursor at the end, the problem is that with this approach the cursor is always forced to any specified selection = TextRange(<index>)(in my case its the end using the current length of the changing value onValueChange), I have no idea how to set the cursor in any part (selection) when I press/click the TextField.
My Implementation:
var textFieldValue by remember { mutableStateOf(TextFieldValue("Some Text")) }
Row (
modifier = Modifier
.height(150.dp)
.fillMaxWidth()
.clickable {
focusRequester.requestFocus()
textFieldValue = textFieldValue.copy(selection = TextRange("Some Text".length))
}
) {
BasicTextField(
modifier = Modifier.focusRequester(focusRequester),
value = textFieldValue,
onValueChange = {
textFieldValue =
textFieldValue.copy(text = it.text, selection = TextRange(it.text.length))
},
)
}
And what I'm trying to achieve (Large text area with text set and starts from top-left), its entire part should be clickable and triggers focus for the text field, the reason why I wrapped it in a Row with a clickable modifier.
I wasn't able to achieve this with a single text field with specified height, as TextAlign doesn't have a Top-Start alignment

Avoid one frame delay in calculating child size

I have to create a reusable component that tries to achieve this goal: I have a column that can have content that's larger than the screen height. On the bottom of the screen we have panel with gradient background that can contain button or something else (it's basically a slot in the component). This bottom panel have to be always visible on the screen, and in case of the column being bigger than screen - bottom panel have to be on the top of this column. Gradient background does a nice UX effect so user knows what is going on. It looks like that:
I have that solved, but here's the challenge. The column content have to be scrollable to be on top of the bottom panel when scrolled to the end. Current solution I have is to add a spacer on the bottom of this column. This spacer have the calculated height of the bottom parent. And here's the issue - right now we have calculation done in onSizeChanged which basically results in additional frame needed for the spacer to have correct size.
We did not observe any negative impact of that performance or UX wise. The spacer height calculation never does anything that user can see, but I still want to solve that properly.
AFAIK this can be done using custom Layout, but that seems a little bit excessive for what I want to achieve. Is there another way to do this properly?
Current solution:
#Composable
fun FloatingPanelColumn(
modifier: Modifier = Modifier,
contentModifier: Modifier = Modifier,
contentHorizontalAlignment: Alignment.Horizontal = Alignment.Start,
bottomPanelContent: #Composable ColumnScope.() -> Unit,
content: #Composable ColumnScope.() -> Unit
) {
val scrollState = rememberScrollState()
var contentSize by remember {
mutableStateOf(1)
}
Box(modifier) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = scrollState)
.then(contentModifier),
horizontalAlignment = contentHorizontalAlignment,
) {
content()
val contentSizeInDp = with(LocalDensity.current) { contentSize.toDp() }
Spacer(modifier = Modifier.height(contentSizeInDp))
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.onSizeChanged {
contentSize = it.height
}
.wrapContentHeight()
.align(Alignment.BottomStart)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0x00FAFCFF),
Color(0xFFF6F9FB),
)
)
),
content = bottomPanelContent
)
}
}
The best way to depend on an other view size during layout is using SubcomposeLayout:
SubcomposeLayout { constraints ->
// subcompose the view you need to measure first
val bottomPanel = subcompose("bottomPanel") {
Column(
// ...
)
}[0].measure(constraints)
// use calculated value in next views layout, like bottomPanel.height
val mainList = subcompose("mainList") {
LazyColumn(
contentPadding = PaddingValues(bottom = bottomPanel.height.toDp())
) {
// ...
}
}[0].measure(constraints)
layout(mainList.width, mainList.height) {
mainList.place(0, 0)
bottomPanel.place(
(mainList.width - bottomPanel.width) / 2,
mainList.height - bottomPanel.height
)
}
}

How to make ClickableText accessible to screen readers

This code creates a ClickableText element in Jetpack Compose Composable:
ClickableText(
text = forgotPasswordAnnotatedString,
onClick = {
context.startActivity(intent)
},
modifier = Modifier
.padding(top = mediumPadding)
)
The annotated string is defined here to make the text look like a link:
val forgotPasswordAnnotatedString = buildAnnotatedString {
append(stringResource(R.string.forgot_password))
addStyle(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
color = Color.White,
fontSize = 16.sp,
fontWeight = FontWeight.Medium
),
start = 0,
end = 21,
)
}
When I encounter this text using the TalkBalk screen reader in Android, the screenreader does not make it clear that this is clickable text that will do something which tapped on. The reader just reads the text.
Is there a way to make it clear to the screen reader that this text is interactive? Otherwise should I just use a button and style it to look like a link?
It looks like your intention is for the whole text to be clickable? In which you best option is probably a TextButton as suggested by
Gabriele Mariotti.
But if you wan't only part of the link to be clickable, or to have multiple clickable sections, the best I've been able to land on is to draw an invisible box overtop of the Text. It means that I can control the touch target of the clickable area to be at least 48.dp and can use the semantics{} modifier to control how a screen reader interprets it.
Would welcome any suggestions.
// remember variables to hold the start and end position of the clickable text
val startX = remember { mutableStateOf(0f) }
val endX = remember { mutableStateOf(0f) }
// convert to Dp and work out width of button
val buttonPaddingX = with(LocalDensity.current) { startX.value.toDp() }
val buttonWidth = with(LocalDensity.current) { (endX.value - startX.value).toDp() }
Text(
text = forgotPasswordAnnotatedString,
onTextLayout = {
startX.value = it.getBoundingBox(0).left // where 0 is the start index of the range you want to be clickable
endX.value = it.getBoundingBox(21 - 1).right // where 21 is the end index of the range you want to be clickable
}
)
Note that buttonPaddingX is relative to the Text position not the Window, so you may have to surround both in a Box{} or use ConstraintLayout.
Then to draw the invisible box
Box(modifier = Modifier
.sizeIn(minWidth = 48.dp, minHeight = 48.dp) // minimum touch target size
.padding(start = buttonPaddingX)
.width(buttonWidth)
// .background(Color.Magenta.copy(alpha = 0.5f)) // uncomment this to debug where the box is drawn
.clickable(onClick = { context.startActivity(intent) })
.semantics {
// tell TalkBack whatever you need to here
role = Role.Button
contentDescription = "Insert button description here"
}
)
In my code I'm using pushStringAnnotation(TAG, annotation) rather than reference string indexes directly. That way I can get the start and end index of the clickable area with annotatedString.getStringAnnotations(TAG,0,annotatedString.length).first(). Useful if there a multiple links within the text.
It's disappointing that ClickableText doesn't have accessibility in mind from the get go, hopefully we'll be able to use it again in a future update.
Adding .semantics.contentDescription to the Modifier changes what is read by the screen reader. I had to word contentDescription to make it clear that this was a link to reset the your password.
The screen reader still doesn't recognize the element a clickable but hopefully the description will be useful to convey to the user that this element is interactive.
ClickableText(
text = forgotPasswordAnnotatedString,
onClick = {
context.startActivity(intent)
},
modifier = Modifier
.padding(top = mediumPadding)
// new code here:
.semantics {
contentDescription = "Forgot your password? link"
}
)

AnimatedVisibility breaks constraints in ConstraintLayout in Jetpack Compose

Edit: Also happens when I swap ConstraintLayout for a Box using alignment as well...
Seems like AnimatedVisibility doesn't play well with ConstraintLayout in JP Compose at the moment.
AnimatedVisibility(
visible = entryListState.firstVisibleItemIndex > 3,
enter = fadeIn() + expandIn(expandFrom = Alignment.Center),
exit = fadeOut() + shrinkOut(shrinkTowards = Alignment.Center)
) {
ExtendedFloatingActionButton(
modifier = Modifier.constrainAs(scrollToTop) {
start.linkTo(parent.start)
bottom.linkTo(parent.bottom)
},
text = { Text(text = "TOP") },
onClick = { scope.launch { entryListState.animateScrollToItem(0) } }
)
}
The ( TOP ) fab should appear in the bottom left corner, but instead I get
When I remove the AnimatedVisibility wrapper, everything works fine. :( I know I can work around this, but I'm curious if there's something I'm doing incorrectly with the configuration of the AnimatedVisibility composable?
Turns out I was thinking about AnimatedVisibility all wrong. It's just another Composable. I needed to lift the FAB's modifiers re: positioning to the AnimatedVisibilty's modifiers.

Resources