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

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

/*
 * Copyright 2020 The Android Open Source Project
 * Modifications Copyright 2022 Peanuuutz
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.peanuuutz.fork.ui.foundation.input

import androidx.compose.runtime.Stable
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import net.peanuuutz.fork.ui.foundation.draw.clip
import net.peanuuutz.fork.ui.ui.context.pointer.PointerEvent
import net.peanuuutz.fork.ui.ui.context.pointer.consumeAllAmount
import net.peanuuutz.fork.ui.ui.context.pointer.isShiftPressed
import net.peanuuutz.fork.ui.ui.layout.Constraints
import net.peanuuutz.fork.ui.ui.layout.Constraints.Companion.Unlimited
import net.peanuuutz.fork.ui.ui.layout.LayoutOrientation
import net.peanuuutz.fork.ui.ui.layout.LayoutOrientation.Horizontal
import net.peanuuutz.fork.ui.ui.layout.LayoutOrientation.Vertical
import net.peanuuutz.fork.ui.ui.layout.Measurable
import net.peanuuutz.fork.ui.ui.layout.MeasureResult
import net.peanuuutz.fork.ui.ui.layout.constrainHeight
import net.peanuuutz.fork.ui.ui.layout.constrainWidth
import net.peanuuutz.fork.ui.ui.modifier.Modifier
import net.peanuuutz.fork.ui.ui.modifier.ModifierNodeElement
import net.peanuuutz.fork.ui.ui.modifier.composed
import net.peanuuutz.fork.ui.ui.modifier.input.SuspendingPointerInputModifierNode
import net.peanuuutz.fork.ui.ui.node.BranchingModifierNode
import net.peanuuutz.fork.ui.ui.node.LayoutCallbackModifierNode
import net.peanuuutz.fork.ui.ui.node.LayoutInfo
import net.peanuuutz.fork.ui.ui.node.LayoutModifierNode
import net.peanuuutz.fork.ui.ui.node.ModifierNode
import net.peanuuutz.fork.ui.ui.node.PointerEventPass
import net.peanuuutz.fork.ui.ui.node.PointerInputModifierNode
import net.peanuuutz.fork.ui.util.MutationPriority

@Stable
fun Modifier.verticalContentScroll(
    mode: ContentScrollMode = ContentScrollMode.Default,
    isEnabled: Boolean = true
): Modifier {
    if (!isEnabled) {
        return this
    }
    return composed {
        verticalContentScroll(
            state = rememberContentScrollState(),
            mode = mode
        )
    }
}

@Stable
fun Modifier.verticalContentScroll(
    state: ContentScrollState,
    mode: ContentScrollMode = ContentScrollMode.Default,
    isEnabled: Boolean = true
): Modifier {
    return this
        .clip()
        .contentScrollable(
            state = state,
            mode = mode,
            isEnabled = isEnabled
        )
        .verticalContentScrollLayout(state)
}

@Stable
fun Modifier.horizontalContentScroll(
    mode: ContentScrollMode = ContentScrollMode.Default,
    isEnabled: Boolean = true
): Modifier {
    if (!isEnabled) {
        return this
    }
    return composed {
        horizontalContentScroll(
            state = rememberContentScrollState(),
            mode = mode
        )
    }
}

@Stable
fun Modifier.horizontalContentScroll(
    state: ContentScrollState,
    mode: ContentScrollMode = ContentScrollMode.Default,
    isEnabled: Boolean = true
): Modifier {
    return this
        .clip()
        .contentScrollable(
            state = state,
            mode = mode,
            isEnabled = isEnabled
        )
        .horizontalContentScrollLayout(state)
}

@Stable
fun Modifier.contentScrollable(
    state: ContentScrollState,
    mode: ContentScrollMode = ContentScrollMode.Default,
    isEnabled: Boolean = true
): Modifier {
    if (!isEnabled) {
        return this
    }
    val element = ContentScrollableModifier(
        mode = mode,
        state = state
    )
    return this then element
}

@Stable
fun Modifier.verticalContentScrollLayout(state: ContentScrollState): Modifier {
    val element = ContentScrollLayoutModifier(
        orientation = Vertical,
        state = state
    )
    return this then element
}

@Stable
fun Modifier.horizontalContentScrollLayout(state: ContentScrollState): Modifier {
    val element = ContentScrollLayoutModifier(
        orientation = Horizontal,
        state = state
    )
    return this then element
}

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

private data class ContentScrollableModifier(
    val mode: ContentScrollMode,
    val state: ContentScrollState
) : ModifierNodeElement() {
    override fun create(): ContentScrollableModifierNode {
        return ContentScrollableModifierNode(
            mode = mode,
            state = state
        )
    }

    override fun update(node: ContentScrollableModifierNode) {
        node.mode = mode
        node.state = state
    }
}

private class ContentScrollableModifierNode(
    var mode: ContentScrollMode,
    var state: ContentScrollState
) : BranchingModifierNode(), PointerInputModifierNode {
    private val pointerInputHandler: SuspendingPointerInputModifierNode = branch {
        SuspendingPointerInputModifierNode {
            coroutineScope {
                detectScroll { scrollEvent ->
                    val state = state
                    when (state.containerOrientation) {
                        null -> return@detectScroll
                        Vertical -> {
                            if (scrollEvent.isShiftPressed) {
                                return@detectScroll
                            }
                        }
                        Horizontal -> {
                            if (scrollEvent.isShiftPressed.not()) {
                                return@detectScroll
                            }
                        }
                    }
                    when (val mode = mode) {
                        is ContentScrollMode.Smooth -> {
                            val velocityModifier = mode.velocityModifier
                            val velocity = scrollEvent.amount * velocityModifier
                            val testAmount = velocity * TestAmountDuration
                            if (state.canScroll(testAmount).not()) {
                                return@detectScroll
                            }
                            // Consume all Amount no matter how much is given
                            scrollEvent.consumeAllAmount()
                            launch {
                                state.animateIncrementalScroll(
                                    velocity = velocity,
                                    velocityDecayAnimationSpec = mode.velocityDecayAnimationSpec,
                                    priority = MutationPriority.User
                                )
                            }
                        }
                        is ContentScrollMode.Snap -> {
                            val amountModifier = mode.amountModifier
                            val amount = scrollEvent.amount * amountModifier
                            val consumedAmount = state.scrollBy(amount)
                            scrollEvent.consumeAmount(consumedAmount / amountModifier)
                        }
                    }
                }
            }
        }
    }

    override fun onPointerEvent(pass: PointerEventPass, pointerEvent: PointerEvent) {
        pointerInputHandler.onPointerEvent(pass, pointerEvent)
    }
}

private data class ContentScrollLayoutModifier(
    val orientation: LayoutOrientation,
    val state: ContentScrollState
) : ModifierNodeElement() {
    override fun create(): ContentScrollLayoutModifierNode {
        return ContentScrollLayoutModifierNode(
            orientation = orientation,
            state = state
        )
    }

    override fun update(node: ContentScrollLayoutModifierNode) {
        node.orientation = orientation
        node.state = state
    }
}

private class ContentScrollLayoutModifierNode(
    var orientation: LayoutOrientation,
    state: ContentScrollState
) : ModifierNode(),
    LayoutModifierNode,
    LayoutCallbackModifierNode
{
    var state: ContentScrollState = state
        set(value) {
            if (field == value) {
                return
            }
            field.detach()
            field = value
        }

    override fun onDetach() {
        state.detach()
    }

    override fun measure(measurable: Measurable, constraints: Constraints): MeasureResult {
        return when (orientation) {
            Vertical -> measureVertical(measurable, constraints)
            Horizontal -> measureHorizontal(measurable, constraints)
        }
    }

    private fun measureVertical(measurable: Measurable, constraints: Constraints): MeasureResult {
        val contentConstraints = constraints.copy(maxHeight = Unlimited)
        val placeable = measurable.measure(contentConstraints)
        val displayHeight = constraints.constrainHeight(placeable.height)
        return MeasureResult(placeable.width, displayHeight) {
            with(state) {
                maxOffset = (placeable.height - displayHeight).toFloat()
                placeable.place(0, -roundedOffset)
            }
        }
    }

    private fun measureHorizontal(measurable: Measurable, constraints: Constraints): MeasureResult {
        val contentConstraints = constraints.copy(maxWidth = Unlimited)
        val placeable = measurable.measure(contentConstraints)
        val displayWidth = constraints.constrainWidth(placeable.width)
        return MeasureResult(displayWidth, placeable.height) {
            with(state) {
                maxOffset = (placeable.width - displayWidth).toFloat()
                placeable.place(-roundedOffset, 0)
            }
        }
    }

    override fun onPlaced(info: LayoutInfo) {
        state.attach(
            info = info,
            orientation = orientation
        )
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy