Is it possible to swap two composables by preserving their states? Let's say I've two TextFields positioned vertically. When a button clicked TextFields should swap their places (one should go down and the other up).
It's pretty easy if you don't need animation:
val state by remember { mutableStateOf(true) }
Column {
if (state) {
TextField1(...)
TextField2(...)
} else {
TextField2(...)
TextField1(...)
}
// Somewhere in Button's onClick
...
state = !state
Without animation
val visible by remember { mutableStateOf(false) }
Box() {
if(visible){
Composable1()
}else{
Composable2()
}
}
With animation
val visible by remember { mutableStateOf(false) }
Box() {
AnimatedVisibility(visible = visible,
enter = fadeIn(tween()), //enter animation
exit = fadeOut(tween(200)){ //exit animation
Composable1()
}
AnimatedVisibility(visible = !visible,
enter = fadeIn(tween()), //enter animation
exit = fadeOut(tween(200)){ //exit animation
Composable2()
}
}
Related
I have an app that has 12 buttons and each one of them has a Character when clicked that Character will be added to a variable called answer which will be displayed in a Text.
I want those buttons to disapear as soon as they are clicked i know i can use something like :
var answer by remember {
mutableStateOf("")
}
var state by remember {
mutableStateOf(true)
}
var alpha by remember {
mutableStateOf(1f)
}
Text(text = answer)
Button(onClick = {
answer=answer.plus(arrayList[0])
alpha = 0f
state = false
},enabled = state,
modifier = Modifier.alpha(alpha)) {
Text(text = arrayList[0])
}
But I don't want to create 2 variables for each button (24 variables) is there a better way to do it pleaase.
I'm using AnimatedContent to animate between two different views but even though I'm specifying I want to use vertical transitions, it's still switching between the view but not applying the actual transition animation. Here's the code:
val isEditState = remember { mutableStateOf(false) }
AnimatedContent(
targetState = isEditState,
transitionSpec = {
(slideInVertically() with slideOutVertically()).using(SizeTransform(clip = false))
}
) { targetState ->
if (targetState.value) {
EditView(...)
} else {
NonEditView(...)
}
}
How can I fix this so the animation works?
If I use AnimatedVisibility it does work but I have to apply it to each view specifically.
AnimatedVisibility(
visible = isEditState.value,
enter = slideInVertically(),
exit = slideOutVertically(),
content = EditView(...)
)
Try using isEditable.value as the target state:
AnimatedContent(
targetState = isEditState.value,
transitionSpec = {
(slideInVertically() with slideOutVertically()).using(SizeTransform(clip = false))
}
) { targetState ->
if (targetState) {
EditView(...)
} else {
NonEditView(...)
}
}
The variable isEditable (which is a State) doesn't change, that's why it doesn't animate. What changes is its value.
I think your error is actually in using
val isEditState = remember { mutableStateOf(false) }
rather than
val isEditState by remember { mutableStateOf(false) }
If you update that to match the standard usage of remember, I think the rest of the solution will fall into place:
val isEditState by remember { mutableStateOf(false) }
AnimatedContent(
targetState = isEditState,
transitionSpec = {
(slideInVertically() with slideOutVertically()).using(SizeTransform(clip = false))
}
) { targetState ->
if (targetState) {
EditView(...)
} else {
NonEditView(...)
}
}
How to create swipe to refresh in Jetpack compose using kotlin? Please Share proper reference link
SwipeRefresh is not available
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { },
) {
LazyColumn {
}
}
Now you can use the built-in pullRefresh modifier.
Something like:
val refreshScope = rememberCoroutineScope()
var refreshing by remember { mutableStateOf(false) }
fun refresh() = refreshScope.launch {
refreshing = true
//...do something
refreshing = false
}
val state = rememberPullRefreshState(refreshing, ::refresh)
Box(Modifier.pullRefresh(state)) {
LazyColumn(Modifier.fillMaxSize()) {
if (!refreshing) {
items(itemCount) {
//...
}
}
}
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
}
To create a swipe-to-refresh layout, we need to add dependency in buld.gradle which will provide swipe to refresh layout just like SwipeRefreshLayout in traditional android.
implementation 'com.google.accompanist:accompanist-swiperefresh:0.24.13-rc'
..
To create this kind of layout we require two APIs one SwipeRefresh for layout and another rememberSwipeRefreshState which will remember the state.
#Composable
fun SwipeRefreshCompose() {
var refreshing by remember { mutableStateOf(false) }
LaunchedEffect(refreshing) {
if (refreshing) {
delay(3000)
refreshing = false
}
}
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing = refreshing),
onRefresh = { refreshing = true },
) {
// list view
}
}
I am using a LazyColumn and there are several items in which one of item has a LaunchedEffect which needs to be executed only when the view is visible.
On the other hand, it gets executed as soon as the LazyColumn is rendered.
How to check whether the item is visible and only then execute the LaunchedEffect?
LazyColumn() {
item {Composable1()}
item {Composable2()}
item {Composable3()}
.
.
.
.
item {Composable19()}
item {Composable20()}
}
Lets assume that Composable19() has a Pager implementation and I want to start auto scrolling once the view is visible by using the LaunchedEffect in this way. The auto scroll is happening even though the view is not visible.
LaunchedEffect(pagerState.currentPage) {
//auto scroll logic
}
LazyScrollState has the firstVisibleItemIndex property. The last visible item can be determined by:
val lastIndex: Int? = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
Then you test to see if the list item index you are interested is within the range. For example if you want your effect to launch when list item 5 becomes visible:
val lastIndex: Int = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
LaunchedEffect((lazyListState.firstVisibleItemIndex > 5 ) && ( 5 < lastIndex)) {
Log.i("First visible item", lazyListState.firstVisibleItemIndex.toString())
// Launch your auto scrolling here...
}
LazyColumn(state = lazyListState) {
}
NOTE: For this to work, DON'T use rememberLazyListState. Instead, create an instance of LazyListState in your viewmodel and pass it to your composable.
If you want to know if an item is visible you can use the LazyListState#layoutInfo that contains information about the visible items.
Since you are reading the state you should use derivedStateOf to avoid redundant recompositions and poor performance
To know if the LazyColumn contains an item you can use:
#Composable
private fun LazyListState.containItem(index:Int): Boolean {
return remember(this) {
derivedStateOf {
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (layoutInfo.totalItemsCount == 0) {
false
} else {
visibleItemsInfo.toMutableList().map { it.index }.contains(index)
}
}
}.value
}
Then you can use:
val state = rememberLazyListState()
LazyColumn(state = state){
//items
}
//Check for a specific item
var isItem2Visible = state.containItem(index = 2)
LaunchedEffect( isItem2Visible){
if (isItem2Visible)
//... item visible do something
else
//... item not visible do something
}
If you want to know all the visible items you can use something similar:
#Composable
private fun LazyListState.visibleItems(): List<Int> {
return remember(this) {
derivedStateOf {
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (layoutInfo.totalItemsCount == 0) {
emptyList()
} else {
visibleItemsInfo.toMutableList().map { it.index }
}
}
}.value
}
Say that I have a button that expands when tapped the first time to reveal text, and performs an action on the second tap--thus acting as a sort of confirmation before performing the action.
#Compose
fun ExpandingConfirmButton(onConfirm: () -> Unit) {
// expanded state of the button
val (expanded, setExpanded) = remember { mutableStateOf(false) }
// animating weight. make the button larger when it is tapped once
val weight = animate(if (expanded) 10F else 2F)
Button(onClick = {
// only fire the action after two taps
if(expanded) {
onConfirm()
}
setExpanded(!expanded)
}) {
Icon(Icons.Dfault.Delete)
// only show the text if the button has already been clicked once,
// otherwise just show the icon
if(expanded) {
Text(text = "Delete", softWrap = false)
}
}
}
And I would use that button like so:
#Composable
fun PersonList(people: List<Person>) {
// some database service exposed via an ambient
val dataService = DataService.current
LazyColumnFor(items = people) {
Row() {
Text(text = it.firstName, modifier = Modifier.weight(10F))
// on the first tap, the button should take up half of the row
ExpandingConfirmButton(onConfirm = { dataService.deletePerson(it) })
}
}
}
This all seems pretty straight-forward. Indeed, before I had split the ExpandingConfirmButton into it's own component and instead had the Button() it wraps directly in my PersonList component, it worked flawlessly.
However, it seems that the Row doesn't quite know what to do with the button when the weight changes when it is inside it's own component. The text inside the button displays, but the size does not change. Does this have to do with the Row's RowScope not getting utilized by the Modifier on the Button component inside ExpandingConfirmButton? If so, how do I go about using it?
What I ended up doing is basically just using the .animateContentSize() modifier.
#Compose
fun ExpandingConfirmButton(onConfirm: () -> Unit) {
// expanded state of the button
val (expanded, setExpanded) = remember { mutableStateOf(false) }
Button(
modifier = Modifier.animateContentSize(), // do this
onClick = {
// only fire the action after two taps
if(expanded) {
onConfirm()
}
setExpanded(!expanded)
}) {
Icon(Icons.Default.Delete)
// only show the text if the button has already been clicked once,
// otherwise just show the icon
if(expanded) {
Text(text = "Delete", softWrap = false)
}
}
}
It really is just that easy. No messing with manual widths, weights, or anything like that.