All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.peanuuutz.fork.ui.foundation.input.AxisDragState.kt Maven / Gradle / Ivy

The newest version!
package net.peanuuutz.fork.ui.foundation.input

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import net.peanuuutz.fork.ui.animation.animate
import net.peanuuutz.fork.ui.animation.spec.target.DefaultFloatAnimationSpec
import net.peanuuutz.fork.ui.animation.spec.target.composite.FiniteAnimationSpec
import net.peanuuutz.fork.ui.ui.unit.FloatOffset
import net.peanuuutz.fork.ui.ui.unit.dot
import net.peanuuutz.fork.ui.ui.unit.isNonZero
import net.peanuuutz.fork.ui.ui.unit.isSpecified
import net.peanuuutz.fork.ui.ui.unit.isZero
import net.peanuuutz.fork.ui.ui.unit.normalize
import net.peanuuutz.fork.ui.util.MutationPriority
import kotlin.math.roundToInt

@Composable
fun rememberAxisDragState(
    initialOffset: Float = 0.0f
): AxisDragState {
    return rememberSaveable(
        saver = AxisDragState.Saver
    ) {
        AxisDragState(initialOffset)
    }
}

@Stable
class AxisDragState(
    initialOffset: Float = 0.0f
) : DragState {
    // -------- States --------

    var offset: Float
        get() = _offset
        set(value) {
            if (_offset == value) {
                return
            }
            _offset = value.coerceAtMost(maxOffset)
        }

    val roundedOffset: Int by derivedStateOf { offset.roundToInt() }

    var maxOffset: Float
        get() = _maxOffset
        set(value) {
            if (_maxOffset == value) {
                return
            }
            _maxOffset = value
            _offset = offset.coerceAtMost(value)
        }

    // -------- Parameters --------

    fun updateAxisDirection(axisDirection: FloatOffset) {
        require(axisDirection.isSpecified() && axisDirection.isNonZero())
        this.axisDirection = axisDirection.normalize()
    }

    // ======== Internal ========

    // -------- Delegate --------

    private val delegate: DragState = DragState { movement ->
        if (movement.isZero()) {
            return@DragState FloatOffset.Zero
        }
        val currentOffset = offset
        val deltaOffset = axisDirection dot movement
        val newOffset = currentOffset + deltaOffset
        val coercedNewOffset = newOffset.coerceIn(0.0f, maxOffset)
        val consumedOffset = coercedNewOffset - currentOffset
        val consumedMovement = movement * (consumedOffset / deltaOffset)
        _offset = coercedNewOffset
        consumedMovement
    }

    override val isDragging: Boolean
        get() = delegate.isDragging

    override suspend fun drag(priority: MutationPriority, scope: suspend DragScope.() -> Unit) {
        delegate.drag(priority, scope)
    }

    override fun dragBy(movement: FloatOffset): FloatOffset {
        return delegate.dragBy(movement)
    }

    // -------- States --------

    private var _offset: Float by mutableStateOf(initialOffset)

    private var _maxOffset: Float by mutableStateOf(0.0f)

    // -------- Parameters --------

    // Users shouldn't get but only update this
    internal var axisDirection: FloatOffset = FloatOffset.Right

    // ======== Public ========

    companion object {
        val Saver: Saver = Saver(
            save = { it.offset },
            restore = { AxisDragState(it) }
        )
    }
}

// -------- States --------

fun AxisDragState.canDrag(deltaOffset: Float): Boolean {
    return offset + deltaOffset in 0.0f..maxOffset
}

fun AxisDragState.canDrag(movement: FloatOffset): Boolean {
    return canDrag(deltaOffset = axisDirection dot movement)
}

// -------- State Operations --------

suspend fun AxisDragState.animateTo(
    targetOffset: Float,
    animationSpec: FiniteAnimationSpec = DefaultFloatAnimationSpec,
    priority: MutationPriority = MutationPriority.Default
): Float {
    var previousAxisMovement = 0.0f
    drag(priority) {
        animate(
            initialValue = 0.0f,
            targetValue = convertTargetOffsetToAxisMovement(targetOffset),
            animationSpec = animationSpec
        ) { offset, _ ->
            val axisDirection = axisDirection
            val movement = axisDirection * (offset - previousAxisMovement)
            previousAxisMovement += axisDirection dot dragBy(movement)
        }
    }
    return previousAxisMovement
}

suspend fun AxisDragState.snapTo(
    targetOffset: Float,
    priority: MutationPriority = MutationPriority.Default
): Float {
    var axisMovement = 0.0f
    drag(priority) {
        val axisDirection = axisDirection
        val providedAxisMovement = convertTargetOffsetToAxisMovement(targetOffset)
        val providedMovement = axisDirection * providedAxisMovement
        axisMovement = axisDirection dot dragBy(providedMovement)
    }
    return axisMovement
}

// ======== Internal ========

// -------- State Operations --------

private fun AxisDragState.convertTargetOffsetToAxisMovement(targetOffset: Float): Float {
    return targetOffset.coerceIn(0.0f, maxOffset) - offset
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy