I have a SwipeToDismiss instance to delete items with dismissThresholds 75%.
If user swipes row too fast without reaching 75% threshold the row being deleted. How to prevent that?
Here is code where I execute an action:
val dismissState = rememberDismissState(
confirmStateChange = {
if (it == DismissValue.DismissedToStart) {
viewModel.deleteCity(city)
}
true
}
)
I ran into the same issue and found a fix that works for me. My theory is, that when the swipe is very fast but doesn't go very far (doesn't reach the set fractional threshold), the layout just immediately resets. In this case (DismissToStart), meaning the view snaps back to the right screen edge, giving us the value of 1.0f for the threshold and thus triggering the confirmStateChange because the fraction is per definition higher than our threshold. The problem being that our threshold is measured from the right screen edge, and this fracion (in my theory) is measured from the left screen edge.
So my solution is to track the current fractional value, and inside confirmStateChange check if the current value is higher than the threshold, BUT NOT 1.0f. In a real world scenario, I think it's impossible to reach 1.0 with actual swiping the finger from right to left, so the solution seems safe to me.
val dismissThreshold = 0.25f
val currentFraction = remember { mutableStateOf(0f) }
val dismissState = rememberDismissState(
confirmStateChange = {
if (it == DismissValue.DismissedToStart) {
if (currentFraction.value >= dismissThreshold && currentFraction.value < 1.0f) {
onSwiped(item)
}
}
dismissOnSwipe
}
)
SwipeToDismiss(
state = dismissState,
modifier = Modifier.animateItemPlacement(),
directions = setOf(DismissDirection.EndToStart),
dismissThresholds = { direction ->
FractionalThreshold(dismissThreshold)
},
background = {
Box(...) {
currentFraction.value = dismissState.progress.fraction
...
}
}
dismissContent = {...}
)
I'm dealing with the same issue, it seems to be a feature, not a bug, but I did not find the way to disable it.
*If the user has dragged their finger across over 50% of the screen
width, the app should trigger the rest of the swipe back animation. If
it's less than that, the app should snap back to the full app view.
If the gesture is quick, ignore the 50% threshold rule and swipe back.*
https://developer.android.com/training/wearables/compose/swipe-to-dismiss
Related
I have a Column with remember scroll state. I am using a derived state as follows.
val isHidden = remember {
derivedStateOf {
columnState.value > 400
}
}
I want to display a search bar whenever it scrolls beyond 400 pixels.
Another approach i used is to create a state based on offset of first item in Column using function onGloballyPositioned.
The modifier of first item is as follows:
Box(modifier = Modifier
.graphicsLayer { translationX = horizontalOffset.value }
.onGloballyPositioned { coordinates ->
isHidden.value = coordinates.boundsInRoot().top == 0f
}) {
The problem is no matter which approach i choose it always lags when the state is changed (when the value checks for < or > 400) or in case of onGloballyPositioned if the Compose reaches top of the screen i.e. y coordinate of view == 0.
In both of the approaches isHidden is going to be true. Changing is hidden in both cases causing lag at the point of change.
I can't use lazy row because of some restrictions. Is there any solution to implement this without the lag.
How do I know a LazyColumn can be navigated?
For example, a list contains 100 elements, then it is a lazy column that can be scroll and we show a fab button to scroll up, but when this list contains 5 elements, the lazy column is not scrollable and the FabButton is no longer needed.
The lazy list layout information can be obtained from state, which is passed to LazyColumn.
In my example, I use derivedStateOf to prevent redundant recompositions - this will cause a recomposition only when the value actually changes. Reading LazyColumn state information without derivedStateOf or side effect function can cause significant performance problems.
The most basic logic in such cases is to check if the first element has been scrolled:
val canScrollToTop by remember {
derivedStateOf {
state.layoutInfo.visibleItemsInfo.run {
isNotEmpty() && (first().index > 0 || first().offset < 0)
}
}
}
LazyColumn(state = state) {
// ...
}
if (canScrollToTop) {
// your button
}
I'm implementing an horizontal pager with Accompanist. Is there an elegant way to automatically switch the images every 5 seconds?
Otherwise, I'd have to fake a manual swipe by using a channel in the viewmodel that increments the currentPage every 5 seconds.. which, to be honest, I'm not quite a fan of.
Before Compose, I used to implement the Why Not Image Carousel, which has a built-in autoplay property.
Any help would be appreciated. Thx!
You can do the following:
val images = // list of your images...
val pageState = rememberPagerState(pageCount = images.size)
HorizontalPager(state = pageState) {
// Your content...
}
// This is the interesting part, when your page changes,
// the LaunchedEffect will run again
LaunchedEffect(pageState.currentPage) {
delay(3000) // wait for 3 seconds.
// increasing the position and check the limit
var newPosition = pageState.currentPage + 1
if (newPosition > images.lastIndex) newPosition = 0
// scrolling to the new position.
pageState.animateScrollToPage(newPosition)
}
Here is the result (in the GIF, the interval is 1 sec):
I'm having troubles with some animation in a recycler view. I do the relevant measurements in onViewAttachedToWindow:
override fun onViewAttachedToWindow(holder: PairingViewHolder) {
super.onViewAttachedToWindow(holder)
// get originalHeight & expandedHeight if not gotten before
if (holder.expandedHeight < 0) {
// Execute pending bindings, otherwise the measurement will be wrong.
holder.itemViewDataBinding.executePendingBindings()
holder.cardContainer.layoutParams.width = expandedWidth
holder.expandedHeight = 0 // so that this block is only called once
holder.cardContainer.doOnLayout { view ->
holder.originalHeight = view.height
holder.expandView.isVisible = true
// show expandView and record expandedHeight in next layout pass
// (doOnPreDraw) and hide it immediately.
view.doOnPreDraw {
holder.expandedHeight = view.height
holder.expandView.isVisible = false
holder.cardContainer.layoutParams.width = originalWidth
}
}
}
}
The problem is that doOnPreDraw gets called just for some views. It is something related to the visibility of the views I guess, since the smaller the items (expanded) are, the highest the count of the ones on which onPreDraw gets called.
My guess is that since I'm expanding them in onLayout, the recyclerView consider visible only the ones that when expanded are actually visible on screen. In onPreDraw I collapse them, resulting in some views being able to animate correctly and some not.
How would you solve this?
Thanks in advance.
So I am new to Kotlin and I am trying to make a super simple app. All it does is when I click Right button it goes right same with left button. The problem is that when I click either button (e.g right button) I can click it until the image goes completely offscreen. So how can I implement a code that once it hits the edge off the screen it stops moving ?
My code
package com.example.change_position_circle
import android.animation.ObjectAnimator
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//val picture = findViewById<ImageView>(R.id.SpongeBob)
val right_button = findViewById<Button>(R.id.right)
val left_button = findViewById<Button>(R.id.left)
right_button.setOnClickListener()
{
//ObjectAnimator.ofFloat(SpongeBob, "x", 100)
SpongeBob.animate().setDuration(90).translationXBy(100f)
}
left_button.setOnClickListener()
{
//ObjectAnimator.ofFloat(SpongeBob, "translationXBy", 100f).apply {
//duration = 200
// start()
SpongeBob.animate().setDuration(90).translationXBy(-100f)
//}
}
}
}
Thank you for your help
Welcome to Kotlin!
So yeah, you've got your Spongebob animating, now you need some logic to control that animation. The problem here is you don't always want that full animation to happen, right? If he's too close to the edge of the screen, you want the button to only move him as far as that invisible wall (and if he's right against it, that means no movement at all).
The animation and drawing systems don't put any restrictions on where you can put a View, so it's up to you to handle that yourself. You basically need to do this when a button is clicked:
get the Spongebob's position coordinates (it's the X you really care about right now)
work out the position of the edge you care about (the View's coordinates describe where the top left corner is, so if you're looking at the right edge, you need that X coordinate + the width of the View)
work out the X coordinate of the edge of the screen (or parent layout, whatever you want the Spongebob to be contained within)
if the distance between the Spongebob edge and the screen edge is less than your normal animation movement, you need to change it to that remaining distance
you'll also want to work out the appropriate duration too, if he's moving half the usual distance the animation should take half as long
that's a lot to work on, and there are a few ways to do it, but here's one approach just using the screen edges as the bounds
import android.os.Bundle
import android.view.View
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import kotlin.math.abs
import kotlin.math.min
private const val MOVE_DISTANCE = 100
private const val MOVE_TIME = 90
class MainActivity : AppCompatActivity() {
private var screenWidth = 0
private lateinit var spongeBob : View
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.spongebob)
// store this when the Activity is created, if the device is rotated the Activity
// will be recreated and this method gets run again
screenWidth = applicationContext.resources.displayMetrics.widthPixels
//val picture = findViewById<ImageView>(R.id.SpongeBob)
val right_button = findViewById<Button>(R.id.right)
val left_button = findViewById<Button>(R.id.left)
spongeBob = findViewById(R.id.spongeBob)
right_button.setOnClickListener()
{
// two possible values - the distance to the edge, and the normal amount we move
// we want the smaller of the two (i.e. always move the normal amount, unless
// the edge is closer than that
val distance = min(distanceToEdge(left = false), MOVE_DISTANCE)
moveSpongeBob(distance)
}
left_button.setOnClickListener()
{
val distance = min(distanceToEdge(left = true), MOVE_DISTANCE)
// we're moving left so we need to use a negative distance
moveSpongeBob (-distance)
}
}
private fun distanceToEdge(left: Boolean): Int {
// Get the Spongebob's top-left position - the call is a method on the View class,
// I'm assuming SpongeBob is a View, and you need to pass an array in because
// that's just how it works for whatever reason...
val location = IntArray(2)
spongeBob.getLocationOnScreen(location)
val x = location[0]
// I'm just using the View.getWidth() call here (Kotlin style) but I don't know
// what the SpongeBob class is, so you'll need to handle this
// You could set this once, like when we get the screen width, but width will be 0 until
// the View is laid out - so you can't do it in #onCreate, #onViewCreated should work
val spongeBobWidth = spongeBob.width
// the left edge is just the x position, however far that is from zero
return if (left) x
// the right edge is the x position plus the width of the bob
else screenWidth - (x + spongeBobWidth)
}
// Actually move the view, by the given distance (negative values to move left)
private fun moveSpongeBob(distance: Int) {
// work out how much this distance relates to our standard move amount, so we can
// adjust the time by the same proportion - converting to float so we don't get
// integer division (where it's rounded to a whole number)
val fraction = distance.toFloat() / MOVE_DISTANCE
// distance can be negative (i.e. moving left) so we need to use the abs function
// to make the duration a postitive number
val duration = abs(MOVE_TIME * fraction).toLong()
spongeBob.animate().setDuration(duration).translationXBy(distance.toFloat())
}
}
There's nicer stuff you can do (and SpongeBob should be called spongeBob and be a View) but that's the basics. This article on the coordinate system might help you out too.