Skip to content

Commit

Permalink
Support defaultOverscrollFactory() on iOS (#1753)
Browse files Browse the repository at this point in the history
Add implementation of OverscrollFactory to support new Overscroll API.
Fix overscroll in CoreTextField and BasicTextField.
Upstreaming issue added:
https://youtrack.jetbrains.com/issue/CMP-7307/Upstreaming.feature.foundation.text.rememberTextFieldOverscrollEffect

Fixes
https://youtrack.jetbrains.com/issue/CMP-7143/Support-OverscrollFactory-and-LocalOverscrollFactory

## Release Notes
### Fixes - iOS
- Enables Cupertino Overscroll by default for scrollable components
- Experimental method`optOutOfCupertinoOverscroll()` removed.
  • Loading branch information
ASalavei authored Dec 20, 2024
1 parent 0c234a4 commit 7eabcc8
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.handwriting.stylusHandwriting
Expand Down Expand Up @@ -313,6 +314,8 @@ internal fun BasicTextField(

DisposableEffect(textFieldSelectionState) { onDispose { textFieldSelectionState.dispose() } }

val overscrollEffect = rememberTextFieldOverscrollEffect()

val handwritingEnabled =
!isPassword &&
keyboardOptions.keyboardType != KeyboardType.Password &&
Expand Down Expand Up @@ -373,6 +376,7 @@ internal fun BasicTextField(
reverseScrolling = false
),
interactionSource = interactionSource,
overscrollEffect = overscrollEffect
)
.pointerHoverIcon(textPointerIcon)

Expand Down Expand Up @@ -401,6 +405,7 @@ internal fun BasicTextField(
)
.textFieldMinSize(textStyle)
.clipToBounds()
.overscroll(overscrollEffect)
.then(
TextFieldCoreModifier(
isFocused = isFocused && isWindowFocused,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.text.handwriting.stylusHandwriting
Expand Down Expand Up @@ -701,6 +702,7 @@ internal fun CoreTextField(
// TextFields
.heightIn(min = state.minHeightForSingleLineField)
.heightInLines(textStyle = textStyle, minLines = minLines, maxLines = maxLines)
.overscroll(overscrollEffect)
.textFieldScroll(
scrollerPosition = scrollerPosition,
textFieldValue = value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,40 @@
package androidx.compose.foundation

import androidx.compose.foundation.cupertino.CupertinoOverscrollEffect
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.UiKitScrollConfig
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalAccessorScope
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection

@Composable
internal actual fun rememberPlatformOverscrollEffect(): OverscrollEffect? =
rememberOverscrollEffect(applyClip = false)

@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun rememberOverscrollEffect(applyClip: Boolean): OverscrollEffect? =
if (UiKitScrollConfig.isRubberBandingOverscrollEnabled) {
val density = LocalDensity.current.density
val layoutDirection = LocalLayoutDirection.current
internal fun rememberOverscrollEffect(applyClip: Boolean): OverscrollEffect {
val density = LocalDensity.current.density
val layoutDirection = LocalLayoutDirection.current

remember(density, layoutDirection) {
CupertinoOverscrollEffect(density, layoutDirection, applyClip)
}
} else {
null
return remember(density, layoutDirection) {
CupertinoOverscrollEffect(density, layoutDirection, applyClip)
}
}

internal actual fun CompositionLocalAccessorScope.defaultOverscrollFactory(): OverscrollFactory? =
// TODO https://youtrack.jetbrains.com/issue/CMP-7143/Support-OverscrollFactory-and-LocalOverscrollFactory
null
internal actual fun CompositionLocalAccessorScope.defaultOverscrollFactory(): OverscrollFactory? {
val density = LocalDensity.currentValue
val layoutDirection = LocalLayoutDirection.currentValue
return CupertinoOverscrollEffectFactory(density, layoutDirection)
}

private data class CupertinoOverscrollEffectFactory(
private val density: Density,
private val layoutDirection: LayoutDirection
) : OverscrollFactory {
override fun createOverscrollEffect(): OverscrollEffect {
return CupertinoOverscrollEffect(density.density, layoutDirection, applyClip = false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import androidx.compose.animation.core.animateTo
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand All @@ -31,8 +30,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.LayoutAwareModifierNode
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.Velocity
Expand Down Expand Up @@ -130,14 +136,11 @@ class CupertinoOverscrollEffect(
// this effect is considered to be in progress
visibleOverscrollOffset.toOffset().getDistance() > 0.5f

override val effectModifier = Modifier
.onPlaced {
scrollSize = it.size.toSize()
}
.clipIfNeeded()
.offset {
visibleOverscrollOffset
}
override val node: DelegatableNode = CupertinoOverscrollNode(
offset = { visibleOverscrollOffset },
coordinates = { scrollSize = it.size.toSize() },
applyClip = applyClip
)

private fun Modifier.clipIfNeeded(): Modifier =
if (applyClip) {
Expand All @@ -148,8 +151,8 @@ class CupertinoOverscrollEffect(

private fun NestedScrollSource.toCupertinoScrollSource(): CupertinoScrollSource? =
when (this) {
NestedScrollSource.Drag -> CupertinoScrollSource.DRAG
NestedScrollSource.Fling -> CupertinoScrollSource.FLING
NestedScrollSource.UserInput -> CupertinoScrollSource.DRAG
NestedScrollSource.SideEffect -> CupertinoScrollSource.FLING
else -> null
}

Expand Down Expand Up @@ -440,6 +443,34 @@ class CupertinoOverscrollEffect(
}
}

private class CupertinoOverscrollNode(
val offset: Density.() -> IntOffset,
val coordinates: (LayoutCoordinates) -> Unit,
val applyClip: Boolean
): LayoutAwareModifierNode, DrawModifierNode, Modifier.Node() {

override val shouldAutoInvalidate: Boolean = true

override fun ContentDrawScope.draw() {
if (applyClip) {
clipRect { this@draw.drawContentWithOffset() }
} else {
this@draw.drawContentWithOffset()
}
}

private fun ContentDrawScope.drawContentWithOffset() {
val offset = offset()
translate(left = offset.x.toFloat(), top = offset.y.toFloat()) {
this@drawContentWithOffset.drawContent()
}
}

override fun onPlaced(coordinates: LayoutCoordinates) {
coordinates(coordinates)
}
}

private fun Velocity.toOffset(): Offset =
Offset(x, y)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,19 @@
* limitations under the License.
*/

@file:Suppress("DEPRECATION")

package androidx.compose.foundation.gestures

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFold

internal actual fun CompositionLocalConsumerModifierNode.platformScrollConfig(): ScrollConfig = UiKitScrollConfig

/**
* Opt out of the Cupertino overscroll behavior (rubber banding and spring effect).
*
* This method should be called before any @Composable function using this effect is executed
* (so as early as possible, e.g. during app start up).
*/
@ExperimentalFoundationApi
fun optOutOfCupertinoOverscroll() {
UiKitScrollConfig.isRubberBandingOverscrollEnabled = false
}

internal object UiKitScrollConfig : ScrollConfig {
var isRubberBandingOverscrollEnabled: Boolean = true

/*
* There are no scroll events produced on iOS,
* so in reality this function should not be ever called.
Expand Down

0 comments on commit 7eabcc8

Please sign in to comment.