Alexander Black
08/07/2021, 2:39 PMHalil Ozercan
08/07/2021, 6:43 PMAlexander Black
08/07/2021, 8:03 PMTin Tran
08/09/2021, 4:53 AMWeightNumbersRow
. Could you explain like I’m 5 😅Halil Ozercan
08/09/2021, 8:40 AM((2f - abs(number - currentValue).coerceAtMost(2f)).pow(2f) + 2f) / 6f
1. abs(number-currentValue)
Number is which "number" we are rendering, "currentValue" is the currently selected value, it comes from state. We first find the absolute distance between them because there is left-right symmetry.
2. distance.coerceAtMost(2f)
Distances that are longer than 2f are ignored and snapped to 2f. This is because we only show 2 numbers to the left and right from currently selected value. The other numbers can have the same scale if they come into view.
3. 2f - limitedDistance
Simply inverse the distance. Closer "numbers" to currentValue will have higher value, farther "numbers" will have lower value.
4. inversedValue.pow(2f)
Make it quadratic, meaning that we want parabolic change instead of linear.
5. parabolicValue + 2f
Parabolic value is in the range of [0, 4]
. 4 is fine but 0 is hard to work with. You cannot multiply anything with 0. Our base should be higher than 0 to make sense.
6. baseFixedValue / 6f
Finally we divide our range to the maximum value -> [0.333f, 1f]
Our new scale is between 0.333 and 1. Farthest numbers are going to be scaled down to 3rd of their size, while dead center item will have unchanged scale.
Hope this clears everything 😄[0.333f, 1f]
.Tin Tran
08/09/2021, 8:45 AMAlexander Black
08/09/2021, 1:37 PMHalil Ozercan
08/09/2021, 1:41 PMTin Tran
08/09/2021, 2:01 PMmodifier.pointerInput(Unit) {
val decay = splineBasedDecay<Float>(this)
val velocityTracker = VelocityTracker()
detectHorizontalDragGestures(
onDragEnd = {
val velocity = velocityTracker.calculateVelocity().x
val target = decay.calculateTargetValue(state.value, velocity) / size.width * -3
state.roundToValue(state.value + target)
},
) { change, dragAmount ->
val scrolledValue = (dragAmount / size.width) * 3
state.changeValueBy(scrolledValue)
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
change.consumePositionChange()
}
fun roundToValue(target: Float) {
coroutineScope.launch {
var boundedTarget = target
if (target <= valueRange.start) {
boundedTarget = valueRange.start
} else if (target >= valueRange.endInclusive) {
boundedTarget = valueRange.endInclusive
}
animatedValue.animateTo(boundedTarget.roundToInt().toFloat(), tween(500))
}
}
It’s not quite what I wanted yet. I want it to slowdown more smoothly like how a scrollable Row does. But this is usable 😅animateDecay
but it didn’t seems to work. The value is not updated so I have to use this ‘hack’ instead 😄Alexander Black
08/09/2021, 3:13 PMTin Tran
08/09/2021, 3:15 PMAlexander Black
08/09/2021, 3:16 PMTin Tran
08/09/2021, 3:18 PMAlexander Black
08/09/2021, 3:18 PMI simply added a fling flingBehavior method to the WeightEntryState class
class WeightEntryState(
initialValue: Float,
var valueRange: ClosedFloatingPointRange<Float>,
private val coroutineScope: CoroutineScope
) {
private val animatedValue = Animatable(initialValue)
val value: Float
get() = animatedValue.value
fun changeValueBy(number: Float) {
animatedValue.updateBounds(valueRange.start, valueRange.endInclusive)
coroutineScope.launch {
animatedValue.snapTo((animatedValue.value - number).coerceIn(valueRange))
}
}
fun flingBehavior(velocity: Float, decayRate: DecayAnimationSpec<Float>){
coroutineScope.launch {
animatedValue.animateDecay(velocity, decayRate)
}
}
fun snapToValue(newValue: Float) {
coroutineScope.launch {
animatedValue.snapTo(newValue)
}
}
}
Tin Tran
08/09/2021, 3:19 PMAlexander Black
08/09/2021, 3:19 PMval velocity by remember { mutableStateOf(VelocityTracker()) }
val animationDecay: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
WeightIndicatorsRow(
state = state,
modifier = Modifier
.background(MyAppTheme.colors.uiBackground)
.pointerInput(Unit) {
detectHorizontalDragGestures(onDragStart = {
velocity.resetTracking()
}, onDragEnd = {
Log.d(LOG_TAG, "velocity x: ${velocity.calculateVelocity().x}")
state.flingBehavior(
velocity = velocity.calculateVelocity().x,
decayRate = animationDecay
)
},
onHorizontalDrag = { change, dragAmount ->
val scrolledValue = (dragAmount / size.width) * 80
state.changeValueBy(scrolledValue)
velocity.addPosition(change.uptimeMillis, Offset(x = dragAmount, 0f))
}
)
})
Tin Tran
08/09/2021, 4:02 PMAlexander Black
08/09/2021, 4:14 PMHalil Ozercan
08/09/2021, 4:15 PMAlexander Black
08/09/2021, 4:15 PMHalil Ozercan
08/09/2021, 4:43 PManimateDecay
callsWeightNumbersRow(
state = state,
modifier = Modifier.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragEnd = {
state.onDragEnd()
}
) { change, dragAmount ->
val scrolledValue = (dragAmount / size.width) * 5
state.changeValueBy(scrolledValue)
state.onDrag(change.uptimeMillis, - (change.position.x / size.width) * 5)
change.consumePositionChange()
}
}
)
// these goes into WeightEntryState
private val decay = exponentialDecay<Float>(
frictionMultiplier = 0.4f,
absVelocityThreshold = 0.05f
)
private val velocityTracker = VelocityTracker()
fun onDrag(durationMillis: Long, position: Float) {
velocityTracker.addPosition(
durationMillis,
Offset(position, 0f)
)
}
fun onDragEnd() {
val velocity = velocityTracker.calculateVelocity().x
coroutineScope.launch {
animatedValue.animateDecay(velocity, decay) {}
roundValue()
}
}
Tin Tran
08/09/2021, 4:50 PMprivate val decay = exponentialDecay<Float>(
frictionMultiplier = 0.4f,
absVelocityThreshold = 0.05f
)
I tried this but can’t figure out what the velocity threshold should be. Thanks a lot 🙌Alexander Black
08/09/2021, 4:59 PMHalil Ozercan
08/09/2021, 5:10 PMTin Tran
08/09/2021, 5:16 PMAlexander Black
08/09/2021, 5:16 PM