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

net.peanuuutz.fork.ui.foundation.input.ContentDragState.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.spec.target.DefaultFloatOffsetAnimationSpec
import net.peanuuutz.fork.ui.animation.spec.target.composite.FiniteAnimationSpec
import net.peanuuutz.fork.ui.ui.layout.Alignment
import net.peanuuutz.fork.ui.ui.modifier.layout.layoutId
import net.peanuuutz.fork.ui.ui.node.LayoutInfo
import net.peanuuutz.fork.ui.ui.unit.FloatOffset
import net.peanuuutz.fork.ui.ui.unit.IntOffset
import net.peanuuutz.fork.ui.ui.unit.IntSize
import net.peanuuutz.fork.ui.ui.unit.coerceIn
import net.peanuuutz.fork.ui.ui.unit.roundToIntOffset
import net.peanuuutz.fork.ui.util.MutationPriority
import net.peanuuutz.fork.util.common.fastFindOrNull
import net.peanuuutz.fork.util.common.pack2Floats
import net.peanuuutz.fork.util.common.unpackFloat1
import net.peanuuutz.fork.util.common.unpackFloat2

@Composable
fun rememberContentDragState(
    initialOffset: FloatOffset = FloatOffset.Zero
): ContentDragState {
    return rememberSaveable(
        saver = ContentDragState.Saver
    ) {
        ContentDragState(initialOffset)
    }
}

@Stable
class ContentDragState(
    initialOffset: FloatOffset = FloatOffset.Zero
) : DragState {
    // -------- States --------

    // ---- Offset ----

    var offset: FloatOffset
        get() = _offset
        set(value) {
            if (_offset == value) {
                return
            }
            _offset = value.coerceIn(FloatOffset.Zero, maxOffset)
        }

    val roundedOffset: IntOffset by derivedStateOf { offset.roundToIntOffset() }

    var maxOffset: FloatOffset
        get() = _maxOffset
        set(value) {
            if (_maxOffset == value) {
                return
            }
            _maxOffset = value
            _offset = offset.coerceIn(FloatOffset.Zero, value)
        }

    // ---- Container ----

    val viewportSize: IntSize?
        get() = _viewportSize

    val contentSize: IntSize?
        get() = _contentSize

    val containerInfo: LayoutInfo?
        get() = _containerInfo

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

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

    private val delegate: DragState = DragState { movement ->
        val currentOffset = offset
        val newOffset = currentOffset - movement
        val coercedNewOffset = newOffset.coerceIn(FloatOffset.Zero, maxOffset)
        val consumedMovement = currentOffset - coercedNewOffset
        _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 --------

    // ---- Offset ----

    private var _offset: FloatOffset by mutableStateOf(initialOffset)

    private var _maxOffset: FloatOffset by mutableStateOf(FloatOffset.Zero)

    // ---- Container ----

    private var _viewportSize: IntSize? by mutableStateOf(null)

    private var _contentSize: IntSize? by mutableStateOf(null)

    private var _containerInfo: LayoutInfo? by mutableStateOf(null)

    internal fun attach(info: LayoutInfo) {
        _viewportSize = info.size
        _contentSize = info.innerInfo!!.size
        _containerInfo = info
    }

    internal fun detach() {
        _viewportSize = null
        _contentSize = null
        _containerInfo = null
    }

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

    companion object {
        val Saver: Saver = Saver(
            save = { state ->
                val offset = state.offset
                pack2Floats(offset.x, offset.y)
            },
            restore = { packedLong ->
                val initialOffset = FloatOffset(
                    x = unpackFloat1(packedLong),
                    y = unpackFloat2(packedLong)
                )
                ContentDragState(initialOffset)
            }
        )
    }
}

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

fun ContentDragState.canDrag(movement: FloatOffset): Boolean {
    val (candidateX, candidateY) = offset + movement
    val (maxX, maxY) = maxOffset
    return candidateX in 0.0f..maxX && candidateY in 0.0f..maxY
}

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

suspend fun ContentDragState.animateToItemByLayoutId(
    layoutId: Any?,
    alignmentInViewport: Alignment = Alignment.CenterOnPlane,
    localOffset: FloatOffset = FloatOffset.Zero,
    animationSpec: FiniteAnimationSpec = DefaultFloatOffsetAnimationSpec,
    priority: MutationPriority = MutationPriority.Default
): FloatOffset {
    return animateToItem(
        alignmentInViewport = alignmentInViewport,
        localOffset = localOffset,
        animationSpec = animationSpec,
        priority = priority
    ) { items ->
        items.fastFindOrNull { it.layoutId == layoutId }
    }
}

suspend fun ContentDragState.snapToItemByLayoutId(
    layoutId: Any?,
    alignmentInViewport: Alignment = Alignment.CenterOnPlane,
    localOffset: FloatOffset = FloatOffset.Zero,
    priority: MutationPriority = MutationPriority.Default
): FloatOffset {
    return snapToItem(
        alignmentInViewport = alignmentInViewport,
        localOffset = localOffset,
        priority = priority
    ) { items ->
        items.fastFindOrNull { it.layoutId == layoutId }
    }
}

suspend fun ContentDragState.animateToItem(
    alignmentInViewport: Alignment = Alignment.CenterOnPlane,
    localOffset: FloatOffset = FloatOffset.Zero,
    animationSpec: FiniteAnimationSpec = DefaultFloatOffsetAnimationSpec,
    priority: MutationPriority = MutationPriority.Default,
    selector: (items: List) -> LayoutInfo?
): FloatOffset {
    val targetOffset = calculateTargetOffsetFromItem(
        alignmentInViewport = alignmentInViewport,
        localOffset = localOffset,
        selector = selector
    )
    return animateTo(
        targetOffset = targetOffset,
        animationSpec = animationSpec,
        priority = priority
    )
}

suspend fun ContentDragState.snapToItem(
    alignmentInViewport: Alignment = Alignment.CenterOnPlane,
    localOffset: FloatOffset = FloatOffset.Zero,
    priority: MutationPriority = MutationPriority.Default,
    selector: (items: List) -> LayoutInfo?
): FloatOffset {
    val targetOffset = calculateTargetOffsetFromItem(
        alignmentInViewport = alignmentInViewport,
        localOffset = localOffset,
        selector = selector
    )
    return snapTo(
        targetOffset = targetOffset,
        priority = priority
    )
}

suspend fun ContentDragState.animateTo(
    targetOffset: FloatOffset,
    animationSpec: FiniteAnimationSpec = DefaultFloatOffsetAnimationSpec,
    priority: MutationPriority = MutationPriority.Default
): FloatOffset {
    return animateDragBy(
        movement = convertTargetOffsetToMovement(targetOffset),
        animationSpec = animationSpec,
        priority = priority
    )
}

suspend fun ContentDragState.snapTo(
    targetOffset: FloatOffset,
    priority: MutationPriority = MutationPriority.Default
): FloatOffset {
    var movement = FloatOffset.Zero
    drag(priority) {
        val providedMovement = convertTargetOffsetToMovement(targetOffset)
        movement = dragBy(providedMovement)
    }
    return movement
}

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

internal const val TestMovementDuration: Float = 0.02f // 20 milliseconds

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

private fun ContentDragState.convertTargetOffsetToMovement(targetOffset: FloatOffset): FloatOffset {
    return targetOffset.coerceIn(FloatOffset.Zero, maxOffset) - offset
}

internal fun ContentDragState.calculateTargetOffsetFromItem(
    alignmentInViewport: Alignment,
    localOffset: FloatOffset,
    selector: (items: List) -> LayoutInfo?
): FloatOffset {
    val currentOffset = offset
    val viewportInfo = containerInfo ?: return currentOffset
    val itemInfo = selector(viewportInfo.childrenInfo) ?: return currentOffset
    val itemPosition = viewportInfo.localPositionOf(itemInfo)
    val alignmentOffset = alignmentInViewport.align(
        contentSize = itemInfo.size,
        availableSpace = viewportInfo.size
    )
    val itemOffset = itemPosition - alignmentOffset
    return currentOffset + itemOffset + localOffset
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy