How to get the threshold value of swipeable jetpack compose? - android

I want threshold value of swipeable item so I can do something when a certain threshold is reached. For example, changing the color of an item swiped empty place.
The swipeable can be added like :
Box(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.swipeable(
state = swipeAbleState,
anchors = anchors,
thresholds = { _, _ ->
FractionalThreshold(.5f)
},
orientation = Orientation.Horizontal
)
)

You can use swipeableState to check current swipe progress and compare it with the threshold. I'm using derivedStateOf to prevent redundant recompositions.
val threshold = 0.3f
val thresholdReached by remember {
derivedStateOf {
swipeableState.progress.from != swipeableState.progress.to // check that we are not in the initial state
&& swipeableState.progress.fraction > threshold
}
}
// ...
thresholds = { _, _ -> FractionalThreshold(threshold) },

Related

How can I detect swipe gestures in Compose for Wear OS (with just 1 function)?

I want to monitor the whole screen and detect if swipe gestures occur.
How can I do this in one function? Easy short and readable would be great.
Would be nice if you explain the steps so I better understand what needs to be done for this.
You can use Modifier.swipeable of WearMaterialApi on your top level container :
val swipeState = SwipeableState<String>("initial")
val anchors = mapOf(0f to "left", 1f to "right")
Column(
modifier = Modifier.swipeable(
state = swipeState,
anchors = anchors,
orientation = Orientation.HORIZONTAL,
enabled = true,
reverseDirection = false,
interactionSource = null,
thresholds = { _, _ ->
FractionalThreshold(0.5f) },
resistance = resistanceConfig(anchors.keys),
velocityThreshold = Dp(200)
)
) {
Text(swipeState.value)
}
It let you define a map of anchors that represent the bounds of the swipe options.
You can make your UI logic around the value of swipstate
In the exemple the text will show right if right swipe occured and left if left swipe occured.
If you want to detect vertical and horizontal swipes at the same times you can use two Modifier.swipeable on a single composable by using Modifier.then like that :
val horizontalSwipeState =
SwipeableState<String>("initial")
val horizontalAnchors = mapOf(0f to "left",
1f to "right")
val verticalSwipeState = SwipeableState<String>("initial")
val verticalAnchors = mapOf(0f to "up", 1f to "down")
Box(
modifier = Modifier
.swipeable(
state = horizontalSwipeState,
anchors = horizontalAnchors,
orientation = Orientation.HORIZONTAL,
enabled = true,
reverseDirection = false,
interactionSource = null,
thresholds = { _, _ -> FractionalThreshold(0.5f) },
resistance = resistanceConfig(horizontalAnchors.keys),
velocityThreshold = Dp(200)
)
.then(
Modifier.swipeable(
state = verticalSwipeState,
anchors = verticalAnchors,
orientation = Orientation.VERTICAL,
enabled = true,
reverseDirection = false,
interactionSource = null,
thresholds = { _, _ -> FractionalThreshold(0.5f) },
resistance = resistanceConfig(verticalAnchors.keys),
velocityThreshold = Dp(200)
)
)
) {
Text("Horizontal:
${horizontalSwipeState.value} Vertical:
${verticalSwipeState.value}")
}
And use a different threshold for each orientation to avoid the two swipes overlaping each others
More info here : https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#(androidx.compose.ui.Modifier).swipeable(androidx.wear.compose.material.SwipeableState,kotlin.collections.Map,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function2,androidx.wear.compose.material.ResistanceConfig,androidx.compose.ui.unit.Dp)

How to set visible indicator' dot and show the others when scrolling in jetpack compose

I have a lazyRow and I want to show list of indicators:
what I want: I want to show 6 items and when user scrolls other indicators get visible.
#Composable
private fun ImagesDotsIndicator(
modifier: Modifier,
totalDots: Int,
selectedIndex: Int
) {
LazyRow(
modifier = modifier,
horizontalArrangement = Arrangement.Center,
reverseLayout = true,
verticalAlignment = Alignment.CenterVertically
) {
if (totalDots == 1) return#LazyRow
items(totalDots) { index ->
if (index == selectedIndex) {
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(color = Color.White)
)
} else {
Box(
modifier = Modifier
.size(6.dp)
.clip(CircleShape)
.background(color = Color.LightGray)
)
}
Spacer(modifier = Modifier.padding(horizontal = 2.dp))
}
}
}
how can I make this indicator?
I would suggest you use Google's Accompanist HorizontalPager and HorizontalPagerIndicator if you want to swipe pages and show the dots. This is a layout that lays out items in a horizontal row, and allows the user to horizontally swipe between pages and also show the page indicator.
You need to add these 2 lines to your app build gradle file to add the dependencies.
// Horizontal Pager and Indicators - Accompanist
implementation "com.google.accompanist:accompanist-pager:0.24.7-alpha"
implementation "com.google.accompanist:accompanist-pager-indicators:0.24.7-alpha"
On your composable file, you can add a simple Sealed class to hold the data that you want to display e.g. text.
sealed class CustomDisplayItem(val text1:String, val text2: String){
object FirstItem: CustomDisplayItem("Hi", "World")
object SecondItem: CustomDisplayItem("Hello", "I'm John")
}
Thereafter make a template of the composable element or page that you want to show if the user swipes left or right.
#Composable
fun DisplayItemTemplate(item: CustomDisplayItem) {
Column() {
Text(text = item.text2 )
Spacer(modifier = Modifier.height(4.dp))
Text(text = item.text2)
}
}
Lastly use HorizontalPager and HorizontalPageIndicator composables to display the corresponding page when a user swipes back and forth.
#OptIn(ExperimentalPagerApi::class)
#Composable
fun ImagesDotsIndicator(
modifier: Modifier,
) {
//list of pages to display
val displayItems = listOf(CustomDisplayItem.FirstItem, CustomDisplayItem.SecondItem)
val state = rememberPagerState()
Column(modifier = modifier.fillMaxSize()) {
//A horizontally scrolling layout that allows users to
// flip between items to the left and right.
HorizontalPager(
count = 6,
state = state,
) {
/*whenever we scroll sideways the page variable changes
displaying the corresponding page */
item ->
//call template item and add the data
DisplayItemTemplate(item = displayItems[item])
}
//HorizontalPagerIndicator dots
HorizontalPagerIndicator(
pagerState = state,
activeColor = MaterialTheme.colors.primary,
inactiveColor = Color.Gray,
indicatorWidth = 16.dp,
indicatorShape = CircleShape,
spacing = 8.dp,
modifier = Modifier
.weight(.1f)
.align(CenterHorizontally)
)
}
}
Please see the above links to read more on how you can customize your composables to work in your case.
Actually it is preaty straight forward without any additional library:
val list = (0..100).toList()
val state = rememberLazyListState()
val visibleIndex by remember {
derivedStateOf {
state.firstVisibleItemIndex
}
}
Text(text = visibleIndex.toString())
LazyColumn(state = state) {
items(list) { item ->
Text(text = item.toString())
}
}
Create scroll state and use it on your list, and on created scroll state observe first visible item.

Changing scroll speed on LazyRow

Looking at the documentation for LazyRow I was wondering if it was possible to reduce the scroll speed, it looks like LazyRow inherits from ScrollState but I can't find anything useful on how to reduce the speed of the scroll
LazyRow doesn't have a parameter to customize scroll speed so you have to do it manually.
You could first capture the scroll gesture something like the below (from google example here ):
#Composable
fun ScrollableSample() {
// actual composable state
var offset by remember { mutableStateOf(0f) }
Box(
Modifier
.size(150.dp)
.scrollable(
orientation = Orientation.Vertical,
// Scrollable state: describes how to consume
// scrolling delta and update offset
state = rememberScrollableState { delta ->
offset += delta
delta
}
)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text(offset.toString())
}
}
Then you have to implement the scroll state at your desired speed for the LazyRow by manually changing the LazyRow's state which is one of the LazyRow parameters.
you will also have to disable LazyRow userscrolling
something like this:
LazyRow(
...
state = yourCustomState,
userScrollEnabled = false,
...
){ ... }
below is a complete working solution:
var stateIsGood = rememberLazyListState()
var offset = 0f
LazyRow(
modifier = Modifier
.scrollable(
orientation = Orientation.Horizontal,
// Scrollable state: describes how to consume
// scrolling delta and update offset
state = rememberScrollableState { delta ->
offset += delta
vm.viewModelScope.launch {
//dividing by 8 the delta for 8 times slower horizontal scroll,
//you can change direction by making it a negative number
stateIsGood.scrollBy(-delta/8)
}
delta
}
),
state = stateIsGood,
userScrollEnabled = false,
) {
item {
Text(text = "Header")
}
// Add 3 items
items(3) { index ->
Text(text = "SCROLL ME First List items : $index")
}
// Add 2 items
items(2) { index ->
Text(text = "Second List Items: $index")
}
// Add another single item
item {
Text(text = "Footer")
}
}
As a shorter alternative to David's Code,
#Composable
fun LazyStack(ssd: Int = 1) { // Scroll-Speed-Divisor
val lazyStackState = rememberLazyListState()
val lazyStackScope = rememberCoroutineScope()
LazyColumn(
modifier = Modifier.pointerInput(Unit) {
detectVerticalDragGestures { _, dragAmount ->
lazyStackScope.launch {
lazyStackState.scrollBy(-dragAmount / ssd).log("checkpoint")
}
}
},
state = lazyStackState,
userScrollEnabled = false
) {...}
}
And yes, LazyStack is short for LazyStackOverflower
instead of prevent userScroll we can use flingBehavior parameter. When we update initialVelocity it should change fling effect.
#Composable
fun maxScrollFlingBehavior(): FlingBehavior {
val flingSpec = rememberSplineBasedDecay<Float>()
return remember(flingSpec) {
ScrollSpeedFlingBehavior(flingSpec)
}
}
private class ScrollSpeedFlingBehavior(
private val flingDecay: DecayAnimationSpec<Float>
) : FlingBehavior {
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
// Prevent very fast scroll
val newVelocity =
if (initialVelocity > 0F) minOf(initialVelocity, 15_000F)
else maxOf(initialVelocity, -15_000F)
return if (abs(newVelocity) > 1f) {
var velocityLeft = newVelocity
var lastValue = 0f
AnimationState(
initialValue = 0f,
initialVelocity = newVelocity,
).animateDecay(flingDecay) {
val delta = value - lastValue
val consumed = scrollBy(delta)
lastValue = value
velocityLeft = this.velocity
// avoid rounding errors and stop if anything is unconsumed
if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
}
velocityLeft
} else newVelocity
}
}
and you can use like this
LazyColumn(
modifier = Modifier.fillMaxSize(),
flingBehavior = maxScrollFlingBehavior()
) {
// Content Here
}
If you want to implement your own logic, you can update initialVelocity param. Higher its 'absolute' value, the faster it moves.

How can I fix toggle animation in Switch (Compose)?

I have a toggle issue with Switch that can look as following:
In the picture you can see only two of many possible states.
I have complex business logics in ViewModel that updates my whole screen state after clicking on switch.
However, in order to make it easier and demonstrate you the problem I found a simple example that is similar to my real-life scenario.
#Composable
fun MyCoolWidget() {
var isChecked by remember { mutableStateOf(true) }
val scope = rememberCoroutineScope()
Switch(
checked = isChecked,
onCheckedChange = {
scope.launch {
delay(50) // to mimic the business logic and state update delay
// I use the 50 millis delay and then update the state
isChecked = it
}
},
)
}
Now you can test it by putting your finger to the one edge, holding the finger on the screen and moving it to the opposite edge. (Don't click on switch, SWIPE it!)
Observe the result.
How can I fix this problem?
Dependencies:
androidx.compose.material:material:1.1.1. Jetpack Compose version - 1.2.0-rc01. Kotlin version - 1.6.21
Thanks, best wishes!
It looks like a bug.
I would code my custom switch if i were you because you have to disable swipe feature.
Here is my custom ios like switch :
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun MySwitch(
modifier: Modifier = Modifier,
isChecked: Int,
onCheckedChange: (checked: Int) -> Unit
) {
var size by remember {
mutableStateOf(IntSize.Zero)
}
val marbleSize by remember(size) {
mutableStateOf(size.height.div(2))
}
val yOffset by remember(size, marbleSize) {
mutableStateOf((size.height.div(2) - marbleSize.div(2f)).toInt())
}
val marblePadding = 4.dp.value
val scope = rememberCoroutineScope()
val swipeableState = rememberSwipeableState(isChecked)
val backgroundColor = animateColorAsState(
targetValue = if (swipeableState.currentValue != 0) Color(0xFF34C759) else Color(0xD6787880)
)
val sizePx = size.width.minus(marbleSize + marblePadding.times(2))
val anchors = mapOf(0f to 0, sizePx - 1f to 1)
LaunchedEffect(key1 = swipeableState.currentValue, block = {
onCheckedChange.invoke(swipeableState.currentValue)
})
Box(
modifier = modifier
.aspectRatio(2f)
.clip(CircleShape)
.swipeable(
state = swipeableState,
anchors = anchors,
enabled = false, //because you need to disable swipe
orientation = Orientation.Horizontal
)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
if (it.x > size.width.div(2))
scope.launch {
swipeableState.animateTo(
1,
anim = tween(250, easing = LinearEasing)
)
}
else
scope.launch {
swipeableState.animateTo(
0,
anim = tween(250, easing = LinearEasing)
)
}
}
)
}
.background(backgroundColor.value)
.onSizeChanged {
size = it
}
) {
Box(
modifier = Modifier
.padding(horizontal = marblePadding.dp)
.offset {
IntOffset(
x = swipeableState.offset.value.roundToInt(),
y = yOffset
)
}
.size(with(LocalDensity.current) { marbleSize.toDp() })
.clip(CircleShape)
.background(Color.Red)
)
}
}
I hope it helps you.

Restore SwipeToDismiss LazyColumn Item to it's original state?

I am able to do SwipeToDismiss but I want to restore the swiped LazyColumn item back to its original state.
I don't want to remove swiped item but want to restore it to its original state.
I am able to achieve this easily in RecyclerView by just calling notifyItemChanged() but can't figure out how to do this in LazyColumn.
Below is my code:
val dataList = remember{ mutableStateListOf<ListItem>()}
for(i in 0..100){
dataList.add(ListItem("$i", "'$i' is the item number."))
}
LazyColumn(Modifier.fillMaxSize()){
items(dataList, key = {it.id}){ item ->
val dismissState = rememberDismissState(
confirmStateChange = {
if(it == DismissedToEnd || it == DismissedToStart){
Handler(Looper.getMainLooper()).postDelayed({
//dataList.remove(item)
}, 1000)
}
true
}
)
SwipeToDismiss(
state = dismissState,
directions = setOf(StartToEnd, EndToStart),
dismissThresholds = { direction ->
FractionalThreshold(if (direction == StartToEnd || direction == EndToStart) 0.25f else 0.5f)
},
background = {
val direction = dismissState.dismissDirection ?: return#SwipeToDismiss
val color by animateColorAsState(
targetValue = when(dismissState.targetValue){
Default -> Color.LightGray
DismissedToEnd -> Color.Green
DismissedToStart -> Color.Red
}
)
val icon = when(direction){
StartToEnd -> Icons.Default.Done
EndToStart -> Icons.Default.Delete
}
val scale by animateFloatAsState(
if (dismissState.targetValue == Default) 0.8f else 1.2f
)
val alignment = when (direction) {
StartToEnd -> Alignment.CenterStart
EndToStart -> Alignment.CenterEnd
}
Box(modifier = Modifier
.fillMaxSize()
.background(color)
.padding(start = 12.dp, end = 12.dp),
contentAlignment = alignment
){
Icon(icon, contentDescription = "Icon", modifier = Modifier.scale(scale))
}
},
dismissContent = {ItemScreen(dismissState = dismissState, item = item)}
)
}
}
You can wait currentValue to become non Default and reset the state:
According to Thinking in Compose, composable function should be free of side effects - you shouldn't directly reset the state in the composable scope. For such situations you need to use one of special side effect functions, more info can be found in side-effects documentation.
Recomposition can happen many times, up to once a frame during animation, and not using side effect functions will lead to multiple calls, which can cause animation problems.
As DismissState.reset() is a suspend function, LaunchedEffect fits perfectly here: it's already running on a coroutine scope.
if (dismissState.currentValue != DismissValue.Default) {
LaunchedEffect(Unit) {
dismissState.reset()
}
}

Categories

Resources