What's the use of `MutableTransitionState`? Looks like mutableStateOf does all it can equally good - android-jetpack-compose

In https://developer.android.com/jetpack/compose/animation, it is mentioned that
We sometimes want to have an initial state different from the first target state. We can use updateTransition with MutableTransitionState to achieve this. For example, it allows us to start animation as soon as the code enters composition.
// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = updateTransition(currentState)
// ...
(For full code, refer to full code example 1. below)
However, the above code is only working for initial animation but not subsequent (i.e. change from Expanded to Collapsed), as whenever the currentState.targetState changes, the composable function will run and retriggered currentState.targetState = BoxState.Expanded.
To fix the problem, I'll have to
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "")
LaunchedEffect(Unit) {
currentState.targetState = BoxState.Expanded
}
(For full code, refer to full code example 2. below)
This will make the animation start automatically from Collapsed to Expanded. Also subsequently upon change from Expanded to Collapse (e.g. through a button click), it will still work, as the LaunchedEffect is no longer triggered.
With the LaunchedEffect, we can also do it with normal mutableStateOf, i.e. without MutableTransitionState, and still, behave well.
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "")
LaunchedEffect(Unit) {
currentState = BoxState.Expanded
}
(For full code, refer to full code example 3. below)
So I cannot see any extra benefit using MutableTransitionState compare to the normal mutableStateOf. Anything I miss?
Full Code that describes the situation above
1. MutableTransitionState code follow document example that doesn't work
The code that doesn't works with just MutableTransitionState (i.e. no effect on button click, only having initial animation)
#Composable
fun Greeting() {
val currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = updateTransition(currentState, label = "")
val rect by transition.animateRect(label = "") { state ->
when (state) {
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
}
}
Column {
Canvas(
modifier = Modifier.fillMaxWidth().height(500.dp)
.border(BorderStroke(1.dp, Color.Green))
) {
drawPath(Path().apply { addRect(rect) }, Color.Red)
}
Button(onClick = {
currentState.targetState =
if (currentState.targetState == BoxState.Expanded) BoxState.Collapsed
else BoxState.Expanded
}) {
Text("Click Me")
}
}
}
2. MutableTransitionState code with launched effect that works
The code that works with just MutableTransitionState (i.e. have effect on button click, and having initial animation)
#Composable
fun Greeting() {
val currentState = remember { MutableTransitionState(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "")
val rect by transition.animateRect(label = "") { state ->
when (state) {
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
}
}
Column {
Canvas(
modifier = Modifier.fillMaxWidth().height(500.dp)
.border(BorderStroke(1.dp, Color.Green))
) {
drawPath(Path().apply { addRect(rect) }, Color.Red)
}
Button(onClick = {
currentState.targetState =
if (currentState.targetState == BoxState.Expanded) BoxState.Collapsed
else BoxState.Expanded
}) {
Text("Click Me")
}
}
LaunchedEffect(Unit) {
currentState.targetState = BoxState.Expanded
}
}
3. Just mutableStateOf code with launched effect that still works
The code that works with just mutableStateOf (i.e. have effect on button click, and having initial animation)
#Composable
fun Greeting() {
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "")
val rect by transition.animateRect(label = "") { state ->
when (state) {
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
}
}
Column {
Canvas(
modifier = Modifier.fillMaxWidth().height(500.dp)
.border(BorderStroke(1.dp, Color.Green))
) {
drawPath(Path().apply { addRect(rect) }, Color.Red)
}
Button(onClick = {
currentState =
if (currentState == BoxState.Expanded) BoxState.Collapsed
else BoxState.Expanded
}) {
Text("Click Me")
}
}
LaunchedEffect(Unit) {
currentState = BoxState.Expanded
}
}

MutableTransitionState allows you to specify a different targetState than the initial state. Note it is not a MutableState.
If you do something like this:
val currentState = remember {
MutableTransitionState(BoxState.Collapsed).apply {targetState = BoxState.Expanded}
}
You can expect the Transition to start an animation going from BoxState.Collapsed to BoxState.Expanded as soon as the updateTransition is composed for the first time.
MutableTransitionState is designed to 1) trigger animations when a composable gets added to the tree (i.e. enter animations), and 2) allow observation of currentState vs targetState through the MutableTransitionState object like this: https://android.googlesource.com/platform/frameworks/support/+/e6095adbb8ffba6aede464fd06ef7302eac61860/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimatedVisiblilityLazyColumnDemo.kt#127

Related

How to add a value coming from inside a compose function into a dataStore

I am building an Android app that uses Compose and Navigation. On one screen I have several form fields each in its own composable function, of which I want to store the values. I have managed to do so for a single form field that is in the main screen function as in this example like this:
#Composable
fun Screen1(navController: NavController) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val dataStoreName = StoreName(context)
val savedName = dataStoreName.getValue.collectAsState(initial = "")
Column( ) {
val patientName = remember { mutableStateOf("") }
Button(onClick = {scope.launch {dataStoreName.saveValue(patientName.value)}
navController.navigate(Screen.Screen2.route) }) {
Text(text = "Save & Next")}
OutlinedTextField( value = patientName.value,
label = { Text(text = "Name") },
onValueChange = { patientName.value = it })
Text(text = "Current information stored: " + savedName.value) } }
However, it is not clear to me how to adapt it when I have several fields each in it's own composable function. When I place the mutableState variable inside the textfield function it is not recognized by the code in the save button, and when I place it outside, the variable is not recognized by the textfield function...
This is one of the textfield function as I have them now, which I would call inside the column in the Screen1 function:
#Composable
fun PatientNameField() {
val patientName by remember { mutableStateOf(TextFieldValue("")) }
OutlinedTextField(
value = patientName.value,
label = { Text(text = "Name") },
onValueChange = { patientName.value = it } )
}
For your current problem you can pass the patientName as a parameter to your PatientNameField composable as shown below. This will help to maintain the state of patientName across your Screen1 and PatientNameField composables.
#Composable
fun Screen1(navController: NavController) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val dataStoreName = StoreName(context)
val savedName = dataStoreName.getValue.collectAsState(initial = "")
Column {
val patientName = remember { mutableStateOf("") }
Button(onClick = {
scope.launch { dataStoreName.saveValue(patientName.value) }
navController.navigate(Screen.Screen2.route)
}) {
Text(text = "Save & Next")
}
//pass the patientName as a parameter to your Composable
PatientNameField(patientName)
Text(text = "Current information stored: " + savedName.value)
}
}
#Composable
fun PatientNameField(patientName: MutableState<String>) {
OutlinedTextField(
value = patientName.value,
label = { Text(text = "Name") },
onValueChange = { patientName.value = it } )
}

Inserting an AnnotatedString into an EditText (Jetpack Compose)

I'm trying to solve the following issue. For example, I'm writing this text - "Hello *world*". After I stop writing, for example, after a second, the word "*world*" should be replaced by "world" in bold.
I've tried to do this, but so far it doesn't work.
val originalText = MutableStateFlow("")
val resultText = originalText
.debounce(1000)
.distinctUntilChanged()
.flatMapLatest { text ->
val result = formatText(text) // create AnnotatedString
flow { emit(result) }
}
And trying to insert to EditText:
val resultText by viewModel.resultText.collectAsState(AnnotatedString(""))
OutlinedTextField(
value = TextFieldValue(resultText),
onValueChange = {
viewModel.originalText.value = it.text
},
label = { Text("Description") },
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
)
The problem is that I can't achieve the following result: we write text to "EditText" and after a second it is formatted and inserted into the same "EditText".
Could someone tell me, please, how can I solve this issue?
I found out a solution, but I'm sorry for code. It's definitely worth improving.
ViewModel methods:
private var _wordList = mutableListOf<String>()
val wordList = _wordList
// Remove words that are not in the string
fun updateWordList(text: String) {
_wordList.forEach {
if(!text.contains(it)) {
_wordList.remove(it)
}
}
}
fun getWords(text: String) : List<String> {
val regex = Regex("\\*(.*?)[\\*]")
val matches = regex.findAll(text)
return matches.map { it.groupValues[1] }.toList()
}
fun addWords(text: String) {
val words = getWords(text)
words.forEach { word ->
if(!_wordList.contains(word)) _wordList.add(word)
}
}
A method which create an AnnotatedString:
fun getAnnotatedString(text: String, words: List<String>): AnnotatedString = buildAnnotatedString {
append(text)
words.forEach { word ->
if (text.contains(word)) {
val offsetStart = text.indexOf(word)
val offsetEnd = offsetStart + word.length
addStyle(
style = SpanStyle(fontWeight = FontWeight.Bold),
start = offsetStart,
end = offsetEnd
)
}
}
}
After that we need to create the following variables:
val words = viewModel.getWords(description)
viewModel.addWords(description)
val descResult = if (words.isEmpty()) description else description.replace("*", "")
val formattedString = formatString(descResult, viewModel.wordList)
var textFieldValueState by remember {
mutableStateOf(TextFieldValue(annotatedString = formattedString))
}
val textFieldValue = textFieldValueState.copy(annotatedString = formattedString)
And finally, we define the OutlinedTextField:
OutlinedTextField(
value = textFieldValue,
onValueChange = {
viewModel.updateWordList(it.text)
if (tmp == it.text) {
textFieldValueState = it
return#OutlinedTextField
}
description = it.text
textFieldValueState = it
},
label = { Text("Description") }
)

Prevent closing bottom sheet with background click with accompanist library

I have tried accompanist library for navigation from this article and I want to prevent bottom sheet to be closed when I click on the background of bottom sheet (the gray area) and to make it non clickable at all, how can I achieve this?
This is the code from the link
#Composable
fun MyApp() {
val navController = rememberNavController()
val bottomSheetNavigator = rememberBottomSheetNavigator()
navController.navigatorProvider += bottomSheetNavigator
ModalBottomSheetLayout(
bottomSheetNavigator = bottomSheetNavigator
) {
NavHost(navController, startDestination = "home") {
composable(route = "home") {
Button(onClick = { navController.navigate("sheet") }) {
Text("Click me to see something cool!")
}
}
bottomSheet(route = "sheet") {
Text("This is a cool bottom sheet!")
Button(onClick = { navController.navigate("home") }) {
Text("Take me back, please!")
}
Spacer(modifier = Modifier.padding(200.dp))
}
}
}
}
ModalBottomSheetLayout has this sheetState parameter set as following:
sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
You can prevent the dismiss behaviour by passing this parameter as following:
// This prevents dismissing the ModalBottomSheet
val sheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
confirmStateChange = { false }
)

Result values in '? :' expression have mismatching types 'some Gesture'

In my view I have a ZStack with a text in it
Text(emoji.text)
.gesture(isEmojiSelected(emoji: emoji) ? moveSelectedEmojis() : moveUnselectedEmoji(emoji: emoji))
private func moveUnselectedEmoji(emoji: EmojiArt.Emoji) -> some Gesture {
DragGesture()
.updating($gestureEmojiMoveOffset) { … }
.onEnded { … }
}
private func moveSelectedEmojis() -> some Gesture {
DragGesture()
.updating($gestureEmojiMoveOffset) { … }
.onEnded { … }
}
I get the compiler error on the .gesture ternary line above, which makes no sense to me because the funcs both return some Gesture:
Result values in '? :' expression have mismatching types 'some Gesture' (result of 'EmojiArtDocumentView.moveSelectedEmojis()') and 'some Gesture' (result of 'EmojiArtDocumentView.moveUnselectedEmoji(emoji:)’)
So:
I change it to this :
Text(emoji.text)
.gesture(moveEmojis(emoji: emoji))
And the func looks like this:
private func moveEmojis(emoji: EmojiArt.Emoji) -> some Gesture {
if (selectedEmojis.count > 0 && isEmojiSelected(emoji: emoji)) {
return moveSelectedEmojis()
} else {
return moveUnselectedEmoji(emoji: emoji)
}
}
I get this compiler error, which I don’t understand because both funcs are essentially identical
Cannot convert return expression of type 'some Gesture' to return type 'AnyGesture'
I also tried making an exact copy of the function name moveSelectedEmojis1 , to try and understand what is happening , but that does not compile either and gets the same error.
.gesture(isEmojiSelected(emoji: emoji) ? moveSelectedEmojis() : moveSelectedEmojis1() )
private func moveSelectedEmojis() -> some Gesture {
DragGesture()
.updating($gestureEmojiMoveOffset) { latestDragGestureValue, gestureEmojiMoveOffset, transaction in
print("gestureEmojiMoveOffset: \(gestureEmojiMoveOffset)")
gestureEmojiMoveOffset = latestDragGestureValue.translation / self.zoomScale
}
.onEnded { finalDragGestureValue in
//print("1steadyStateEmojiMoveOffset: \(steadyStateEmojiMoveOffset) ")
self.steadyStateEmojiMoveOffset = self.steadyStateEmojiMoveOffset + (finalDragGestureValue.translation / self.zoomScale)
print("steadyStateEmojiMoveOffset: \(steadyStateEmojiMoveOffset) ")
var tmp: Set<EmojiArt.Emoji> = []
for emoji in selectedEmojis {
document.moveEmoji(emoji, by: steadyStateEmojiMoveOffset)
tmp.insert(document.updateEmojiLocation(emoji, by: steadyStateEmojiMoveOffset))
}
selectedEmojis = tmp
steadyStateEmojiMoveOffset = .zero
print("moveended selected Emojis: \(selectedEmojis)")
}
}
private func moveSelectedEmojis1() -> some Gesture {
DragGesture()
.updating($gestureEmojiMoveOffset) { latestDragGestureValue, gestureEmojiMoveOffset, transaction in
print("gestureEmojiMoveOffset: \(gestureEmojiMoveOffset)")
gestureEmojiMoveOffset = latestDragGestureValue.translation / self.zoomScale
}
.onEnded { finalDragGestureValue in
//print("1steadyStateEmojiMoveOffset: \(steadyStateEmojiMoveOffset) ")
self.steadyStateEmojiMoveOffset = self.steadyStateEmojiMoveOffset + (finalDragGestureValue.translation / self.zoomScale)
print("steadyStateEmojiMoveOffset: \(steadyStateEmojiMoveOffset) ")
var tmp: Set<EmojiArt.Emoji> = []
for emoji in selectedEmojis {
document.moveEmoji(emoji, by: steadyStateEmojiMoveOffset)
tmp.insert(document.updateEmojiLocation(emoji, by: steadyStateEmojiMoveOffset))
}
selectedEmojis = tmp
steadyStateEmojiMoveOffset = .zero
print("moveended selected Emojis: \(selectedEmojis)")
}
}

Jetpack Compose list diffs animation

Is there a way to get an animation effect on a list (column/row) changes in Compose that looks something like recyclerview animations with setItemAnimator?
There is not currently a way to do this with LazyColumn/LazyRow. This is something that is likely to be added eventually (though as always with predictions about the future: no promises), but it's currently a lower priority than getting more fundamental features working.
Note: I work on the team that implemented these components. I'll update this answer if the situation changes.
The Modifier API called Modifier.animateItemPlacement() was implemented and merged and will probably be released in an upcoming Compose version. Tweet: https://twitter.com/CatalinGhita4/status/1455500904690552836?s=20
At the moment, you'll need to manage the enter/exit transition of the changed items explicitly. You could use AnimatedVisibility for that like this example.
Here's an example for dealing with item additions/removals at least:
#ExperimentalAnimationApi
#Suppress("UpdateTransitionLabel", "TransitionPropertiesLabel")
#SuppressLint("ComposableNaming", "UnusedTransitionTargetStateParameter")
/**
* #param state Use [updateAnimatedItemsState].
*/
inline fun <T> LazyListScope.animatedItemsIndexed(
state: List<AnimatedItem<T>>,
enterTransition: EnterTransition = expandVertically(),
exitTransition: ExitTransition = shrinkVertically(),
noinline key: ((item: T) -> Any)? = null,
crossinline itemContent: #Composable LazyItemScope.(index: Int, item: T) -> Unit
) {
items(
state.size,
if (key != null) { keyIndex: Int -> key(state[keyIndex].item) } else null
) { index ->
val item = state[index]
val visibility = item.visibility
androidx.compose.runtime.key(key?.invoke(item.item)) {
AnimatedVisibility(
visibleState = visibility,
enter = enterTransition,
exit = exitTransition
) {
itemContent(index, item.item)
}
}
}
}
#Composable
fun <T> updateAnimatedItemsState(
newList: List<T>
): State<List<AnimatedItem<T>>> {
val state = remember { mutableStateOf(emptyList<AnimatedItem<T>>()) }
LaunchedEffect(newList) {
if (state.value == newList) {
return#LaunchedEffect
}
val oldList = state.value.toList()
val diffCb = object : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldList.size
override fun getNewListSize(): Int = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition].item == newList[newItemPosition]
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition].item == newList[newItemPosition]
}
val diffResult = calculateDiff(false, diffCb)
val compositeList = oldList.toMutableList()
diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
override fun onInserted(position: Int, count: Int) {
for (i in 0 until count) {
val newItem = AnimatedItem(visibility = MutableTransitionState(false), newList[position + i])
newItem.visibility.targetState = true
compositeList.add(position + i, newItem)
}
}
override fun onRemoved(position: Int, count: Int) {
for (i in 0 until count) {
compositeList[position + i].visibility.targetState = false
}
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
// not detecting moves.
}
override fun onChanged(position: Int, count: Int, payload: Any?) {
// irrelevant with compose.
}
})
if (state.value != compositeList) {
state.value = compositeList
}
val initialAnimation = Animatable(1.0f)
initialAnimation.animateTo(0f)
state.value = state.value.filter { it.visibility.targetState }
}
return state
}
data class AnimatedItem<T>(
val visibility: MutableTransitionState<Boolean>,
val item: T,
) {
override fun hashCode(): Int {
return item?.hashCode() ?: 0
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AnimatedItem<*>
if (item != other.item) return false
return true
}
}
suspend fun calculateDiff(
detectMoves: Boolean = true,
diffCb: DiffUtil.Callback
): DiffUtil.DiffResult {
return withContext(Dispatchers.Unconfined) {
DiffUtil.calculateDiff(diffCb, detectMoves)
}
}

Resources