
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