From 7a040af76422cc4d4695ae2b123988d413ca28a1 Mon Sep 17 00:00:00 2001 From: Colin White Date: Wed, 24 Jul 2024 15:32:55 -0400 Subject: [PATCH] Add support for an onScroll property to Row and Column. (#2067) * Add support for an onScroll property to Row and Column. * Update changelog. * Add tests. * Update API. * Update redwood-layout-view/src/main/kotlin/app/cash/redwood/layout/view/ViewFlexContainer.kt Co-authored-by: Jake Wharton * Update redwood-layout-view/src/main/kotlin/app/cash/redwood/layout/view/ViewFlexContainer.kt Co-authored-by: Jake Wharton * Fixes. * Lint. * Fixes. * Update redwood api. * Add screenshots. * Update API. * Fix screenshot test. * Enforce size. * Update changelog. --------- Co-authored-by: Jake Wharton --- CHANGELOG.md | 1 + .../api/redwood-layout-compose.api | 4 +-- .../api/redwood-layout-compose.klib.api | 4 +-- .../composeui/ComposeUiFlexContainerTest.kt | 13 +++++++++ .../composeui/ComposeUiFlexContainer.kt | 27 ++++++++++++++++-- ...ontainerTest_testOnScrollListener[LTR].png | 3 ++ ...ontainerTest_testOnScrollListener[RTL].png | 3 ++ .../HTMLElementRedwoodLayoutWidgetFactory.kt | 25 +++++++++++++++++ .../api/redwood-layout-schema.api | 16 +++++++---- redwood-layout-schema/redwood-api.xml | 2 ++ .../kotlin/app/cash/redwood/layout/widgets.kt | 20 +++++++++++++ .../layout/AbstractFlexContainerTest.kt | 24 ++++++++++++++++ .../api/redwood-layout-testing.api | 10 ++++--- .../api/redwood-layout-testing.klib.api | 8 ++++-- .../testOnScrollListener.1.png | 3 ++ .../layout/uiview/UIViewFlexContainer.kt | 6 +++- .../cash/redwood/layout/uiview/YogaUIView.kt | 23 +++++++++++++-- .../layout/uiview/UIViewFlexContainerTest.kt | 11 ++++++++ .../redwood/layout/view/ViewFlexContainer.kt | 28 +++++++++++++++++++ .../layout/view/ViewFlexContainerTest.kt | 11 ++++++++ ...ontainerTest_testOnScrollListener[LTR].png | 3 ++ ...ontainerTest_testOnScrollListener[RTL].png | 3 ++ .../api/redwood-layout-widget.api | 2 ++ .../api/redwood-layout-widget.klib.api | 2 ++ .../layout/composeui/ComposeUiLazyListTest.kt | 13 +++++++++ ...LazyListTest_testOnScrollListener[LTR].png | 3 ++ ...LazyListTest_testOnScrollListener[RTL].png | 3 ++ .../lazylayout/view/ViewLazyListTest.kt | 13 +++++++++ ...LazyListTest_testOnScrollListener[LTR].png | 3 ++ ...LazyListTest_testOnScrollListener[RTL].png | 3 ++ .../emojisearch/presenter/EmojiSearch.kt | 3 +- 31 files changed, 271 insertions(+), 22 deletions(-) create mode 100644 redwood-layout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiFlexContainerTest_testOnScrollListener[LTR].png create mode 100644 redwood-layout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiFlexContainerTest_testOnScrollListener[RTL].png create mode 100644 redwood-layout-uiview/RedwoodLayoutUIViewTests/__Snapshots__/UIViewFlexContainerTestHost/testOnScrollListener.1.png create mode 100644 redwood-layout-view/src/test/snapshots/images/app.cash.redwood.layout.view_ViewFlexContainerTest_testOnScrollListener[LTR].png create mode 100644 redwood-layout-view/src/test/snapshots/images/app.cash.redwood.layout.view_ViewFlexContainerTest_testOnScrollListener[RTL].png create mode 100644 redwood-lazylayout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiLazyListTest_testOnScrollListener[LTR].png create mode 100644 redwood-lazylayout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiLazyListTest_testOnScrollListener[RTL].png create mode 100644 redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testOnScrollListener[LTR].png create mode 100644 redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testOnScrollListener[RTL].png diff --git a/CHANGELOG.md b/CHANGELOG.md index ec611a137d..a883a37e66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ New: - Wasm JS added as a target for common Redwood modules. There is no Treehouse support today. +- Add `onScroll` property to `Row` and `Column`. This property is invoked when `overflow = Overflow.Scroll` and the container is scrolled. Changed: - The `TreehouseApp` type is now an abstract class. This should make it easier to write unit tests for code that integrates Treehouse. diff --git a/redwood-layout-compose/api/redwood-layout-compose.api b/redwood-layout-compose/api/redwood-layout-compose.api index a012b5b70d..04aeada4d4 100644 --- a/redwood-layout-compose/api/redwood-layout-compose.api +++ b/redwood-layout-compose/api/redwood-layout-compose.api @@ -12,7 +12,7 @@ public abstract interface class app/cash/redwood/layout/compose/BoxScope { } public final class app/cash/redwood/layout/compose/ColumnKt { - public static final fun Column-n_TNbJ4 (IILapp/cash/redwood/ui/Margin;IIILapp/cash/redwood/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V + public static final fun Column-TKMhg4E (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lapp/cash/redwood/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V } public abstract interface class app/cash/redwood/layout/compose/ColumnScope { @@ -27,7 +27,7 @@ public abstract interface class app/cash/redwood/layout/compose/ColumnScope { } public final class app/cash/redwood/layout/compose/RowKt { - public static final fun Row-dhzo6EI (IILapp/cash/redwood/ui/Margin;IIILapp/cash/redwood/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V + public static final fun Row-5pIFpiI (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lapp/cash/redwood/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V } public abstract interface class app/cash/redwood/layout/compose/RowScope { diff --git a/redwood-layout-compose/api/redwood-layout-compose.klib.api b/redwood-layout-compose/api/redwood-layout-compose.klib.api index 14614b48c7..772a5c9636 100644 --- a/redwood-layout-compose/api/redwood-layout-compose.klib.api +++ b/redwood-layout-compose/api/redwood-layout-compose.klib.api @@ -51,6 +51,6 @@ final val app.cash.redwood.layout.compose/app_cash_redwood_layout_compose_Vertic final val app.cash.redwood.layout.compose/app_cash_redwood_layout_compose_WidthImpl$stableprop // app.cash.redwood.layout.compose/app_cash_redwood_layout_compose_WidthImpl$stableprop|#static{}app_cash_redwood_layout_compose_WidthImpl$stableprop[0] final fun app.cash.redwood.layout.compose/Box(app.cash.redwood.layout.api/Constraint?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.ui/Margin?, app.cash.redwood.layout.api/CrossAxisAlignment?, app.cash.redwood.layout.api/CrossAxisAlignment?, app.cash.redwood/Modifier?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.layout.compose/Box|Box(app.cash.redwood.layout.api.Constraint?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.ui.Margin?;app.cash.redwood.layout.api.CrossAxisAlignment?;app.cash.redwood.layout.api.CrossAxisAlignment?;app.cash.redwood.Modifier?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] -final fun app.cash.redwood.layout.compose/Column(app.cash.redwood.layout.api/Constraint?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.ui/Margin?, app.cash.redwood.layout.api/Overflow?, app.cash.redwood.layout.api/CrossAxisAlignment?, app.cash.redwood.layout.api/MainAxisAlignment?, app.cash.redwood/Modifier?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.layout.compose/Column|Column(app.cash.redwood.layout.api.Constraint?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.ui.Margin?;app.cash.redwood.layout.api.Overflow?;app.cash.redwood.layout.api.CrossAxisAlignment?;app.cash.redwood.layout.api.MainAxisAlignment?;app.cash.redwood.Modifier?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] -final fun app.cash.redwood.layout.compose/Row(app.cash.redwood.layout.api/Constraint?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.ui/Margin?, app.cash.redwood.layout.api/Overflow?, app.cash.redwood.layout.api/MainAxisAlignment?, app.cash.redwood.layout.api/CrossAxisAlignment?, app.cash.redwood/Modifier?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.layout.compose/Row|Row(app.cash.redwood.layout.api.Constraint?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.ui.Margin?;app.cash.redwood.layout.api.Overflow?;app.cash.redwood.layout.api.MainAxisAlignment?;app.cash.redwood.layout.api.CrossAxisAlignment?;app.cash.redwood.Modifier?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] +final fun app.cash.redwood.layout.compose/Column(app.cash.redwood.layout.api/Constraint?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.ui/Margin?, app.cash.redwood.layout.api/Overflow?, app.cash.redwood.layout.api/CrossAxisAlignment?, app.cash.redwood.layout.api/MainAxisAlignment?, kotlin/Function1?, app.cash.redwood/Modifier?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.layout.compose/Column|Column(app.cash.redwood.layout.api.Constraint?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.ui.Margin?;app.cash.redwood.layout.api.Overflow?;app.cash.redwood.layout.api.CrossAxisAlignment?;app.cash.redwood.layout.api.MainAxisAlignment?;kotlin.Function1?;app.cash.redwood.Modifier?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] +final fun app.cash.redwood.layout.compose/Row(app.cash.redwood.layout.api/Constraint?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.ui/Margin?, app.cash.redwood.layout.api/Overflow?, app.cash.redwood.layout.api/MainAxisAlignment?, app.cash.redwood.layout.api/CrossAxisAlignment?, kotlin/Function1?, app.cash.redwood/Modifier?, kotlin/Function3, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.layout.compose/Row|Row(app.cash.redwood.layout.api.Constraint?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.ui.Margin?;app.cash.redwood.layout.api.Overflow?;app.cash.redwood.layout.api.MainAxisAlignment?;app.cash.redwood.layout.api.CrossAxisAlignment?;kotlin.Function1?;app.cash.redwood.Modifier?;kotlin.Function3;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun app.cash.redwood.layout.compose/Spacer(app.cash.redwood.ui/Dp, app.cash.redwood.ui/Dp, app.cash.redwood/Modifier?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.layout.compose/Spacer|Spacer(app.cash.redwood.ui.Dp;app.cash.redwood.ui.Dp;app.cash.redwood.Modifier?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] diff --git a/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainerTest.kt b/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainerTest.kt index 12f040daa4..ef5203e7af 100644 --- a/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainerTest.kt +++ b/redwood-layout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainerTest.kt @@ -30,6 +30,7 @@ import app.cash.redwood.yoga.FlexDirection import com.android.resources.LayoutDirection import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector +import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.runner.RunWith @@ -69,13 +70,25 @@ class ComposeUiFlexContainerTest( ) : TestFlexContainer<@Composable () -> Unit>, FlexContainer<@Composable () -> Unit> by delegate { private var childCount = 0 + override val children: ComposeWidgetChildren = delegate.children + constructor(direction: FlexDirection, backgroundColor: Int) : this( ComposeUiFlexContainer(direction).apply { testOnlyModifier = Modifier.background(Color(backgroundColor)) }, ) + override fun onScroll(onScroll: ((Double) -> Unit)?) { + delegate.onScroll(onScroll) + } + + override fun scroll(offset: Double) { + runBlocking { + delegate.scrollState?.scrollTo(offset.toInt()) + } + } + override fun add(widget: Widget<@Composable () -> Unit>) { addAt(childCount, widget) } diff --git a/redwood-layout-composeui/src/commonMain/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainer.kt b/redwood-layout-composeui/src/commonMain/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainer.kt index b9f19437ad..21b13ed3f4 100644 --- a/redwood-layout-composeui/src/commonMain/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainer.kt +++ b/redwood-layout-composeui/src/commonMain/kotlin/app/cash/redwood/layout/composeui/ComposeUiFlexContainer.kt @@ -15,6 +15,7 @@ */ package app.cash.redwood.layout.composeui +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth @@ -23,9 +24,12 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -65,9 +69,11 @@ internal class ComposeUiFlexContainer( private var height by mutableStateOf(Constraint.Wrap) private var overflow by mutableStateOf(Overflow.Clip) private var margin by mutableStateOf(Margin.Zero) + private var onScroll: ((Double) -> Unit)? by mutableStateOf(null) override var density = Density(1.0) internal var testOnlyModifier: Modifier? = null + internal var scrollState: ScrollState? = null override fun width(width: Constraint) { this.width = width @@ -85,6 +91,10 @@ internal class ComposeUiFlexContainer( this.overflow = overflow } + override fun onScroll(onScroll: ((Double) -> Unit)?) { + this.onScroll = onScroll + } + override fun crossAxisAlignment(crossAxisAlignment: CrossAxisAlignment) { super.crossAxisAlignment(crossAxisAlignment) invalidate() @@ -136,16 +146,29 @@ internal class ComposeUiFlexContainer( modifier.wrapContentHeight(Alignment.Top, unbounded = true) } if (overflow == Overflow.Scroll) { + val scrollState = rememberScrollState().also { scrollState = it } if (flexDirection.isHorizontal) { - modifier = modifier.horizontalScroll(rememberScrollState()) + modifier = modifier.horizontalScroll(scrollState) } else { - modifier = modifier.verticalScroll(rememberScrollState()) + modifier = modifier.verticalScroll(scrollState) } + ObserveScrollState(scrollState) } testOnlyModifier?.let { modifier = modifier.then(it) } return modifier } + @Composable + private fun ObserveScrollState(scrollState: ScrollState) { + val onScroll = onScroll + if (onScroll != null) { + val offset by remember { derivedStateOf { scrollState.value.toDouble() } } + LaunchedEffect(offset) { + onScroll(offset) + } + } + } + private fun measure( scope: MeasureScope, measurables: List, diff --git a/redwood-layout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiFlexContainerTest_testOnScrollListener[LTR].png b/redwood-layout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiFlexContainerTest_testOnScrollListener[LTR].png new file mode 100644 index 0000000000..784d7fc152 --- /dev/null +++ b/redwood-layout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiFlexContainerTest_testOnScrollListener[LTR].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57e6bb80bdab38226d042e9f722c4b0951a83ca348ef237b1b5e7e64d5770def +size 3836 diff --git a/redwood-layout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiFlexContainerTest_testOnScrollListener[RTL].png b/redwood-layout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiFlexContainerTest_testOnScrollListener[RTL].png new file mode 100644 index 0000000000..784d7fc152 --- /dev/null +++ b/redwood-layout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiFlexContainerTest_testOnScrollListener[RTL].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57e6bb80bdab38226d042e9f722c4b0951a83ca348ef237b1b5e7e64d5770def +size 3836 diff --git a/redwood-layout-dom/src/commonMain/kotlin/app/cash/redwood/layout/dom/HTMLElementRedwoodLayoutWidgetFactory.kt b/redwood-layout-dom/src/commonMain/kotlin/app/cash/redwood/layout/dom/HTMLElementRedwoodLayoutWidgetFactory.kt index d7230d5c6c..79b8668eca 100644 --- a/redwood-layout-dom/src/commonMain/kotlin/app/cash/redwood/layout/dom/HTMLElementRedwoodLayoutWidgetFactory.kt +++ b/redwood-layout-dom/src/commonMain/kotlin/app/cash/redwood/layout/dom/HTMLElementRedwoodLayoutWidgetFactory.kt @@ -40,6 +40,8 @@ import app.cash.redwood.widget.Widget import org.w3c.dom.Document import org.w3c.dom.HTMLDivElement import org.w3c.dom.HTMLElement +import org.w3c.dom.events.Event +import org.w3c.dom.events.EventListener public class HTMLElementRedwoodLayoutWidgetFactory( private val document: Document, @@ -80,6 +82,8 @@ private class HTMLFlexContainer( override val children: Widget.Children = HTMLFlexElementChildren(value) + private var scrollEventListener: EventListener? = null + override fun width(width: Constraint) { value.style.width = width.toCss() } @@ -101,6 +105,27 @@ private class HTMLFlexContainer( value.overflowSetter(overflow.toCss()) } + override fun onScroll(onScroll: ((Double) -> Unit)?) { + scrollEventListener?.let { eventListener -> + value.removeEventListener("scroll", eventListener) + scrollEventListener = null + } + + if (onScroll != null) { + val eventListener = object : EventListener { + override fun handleEvent(event: Event) { + val offset = when (value.style.flexDirection) { + "row" -> value.scrollTop + "column" -> value.scrollLeft + else -> throw AssertionError() + } + onScroll(offset) + } + }.also { scrollEventListener = it } + value.addEventListener("scroll", eventListener) + } + } + override fun crossAxisAlignment(crossAxisAlignment: CrossAxisAlignment) { value.style.alignItems = crossAxisAlignment.toCss() } diff --git a/redwood-layout-schema/api/redwood-layout-schema.api b/redwood-layout-schema/api/redwood-layout-schema.api index a17916b074..3a9f2bd544 100644 --- a/redwood-layout-schema/api/redwood-layout-schema.api +++ b/redwood-layout-schema/api/redwood-layout-schema.api @@ -24,7 +24,7 @@ public final class app/cash/redwood/layout/BoxScope { } public final class app/cash/redwood/layout/Column { - public synthetic fun (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-jfchsNQ ()I public final fun component2-jfchsNQ ()I public final fun component3 ()Lapp/cash/redwood/ui/Margin; @@ -32,13 +32,15 @@ public final class app/cash/redwood/layout/Column { public final fun component5-eoX4IBc ()I public final fun component6-Ng1ngeI ()I public final fun component7 ()Lkotlin/jvm/functions/Function1; - public final fun copy-bVM0h8I (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;)Lapp/cash/redwood/layout/Column; - public static synthetic fun copy-bVM0h8I$default (Lapp/cash/redwood/layout/Column;IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/cash/redwood/layout/Column; + public final fun component8 ()Lkotlin/jvm/functions/Function1; + public final fun copy-9Hx0b_M (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/cash/redwood/layout/Column; + public static synthetic fun copy-9Hx0b_M$default (Lapp/cash/redwood/layout/Column;IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/cash/redwood/layout/Column; public fun equals (Ljava/lang/Object;)Z public final fun getChildren ()Lkotlin/jvm/functions/Function1; public final fun getHeight-jfchsNQ ()I public final fun getHorizontalAlignment-eoX4IBc ()I public final fun getMargin ()Lapp/cash/redwood/ui/Margin; + public final fun getOnScroll ()Lkotlin/jvm/functions/Function1; public final fun getOverflow-Andb1w4 ()I public final fun getVerticalAlignment-Ng1ngeI ()I public final fun getWidth-jfchsNQ ()I @@ -109,7 +111,7 @@ public abstract interface class app/cash/redwood/layout/RedwoodLayout { } public final class app/cash/redwood/layout/Row { - public synthetic fun (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-jfchsNQ ()I public final fun component2-jfchsNQ ()I public final fun component3 ()Lapp/cash/redwood/ui/Margin; @@ -117,13 +119,15 @@ public final class app/cash/redwood/layout/Row { public final fun component5-Ng1ngeI ()I public final fun component6-eoX4IBc ()I public final fun component7 ()Lkotlin/jvm/functions/Function1; - public final fun copy-DTaUb5E (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;)Lapp/cash/redwood/layout/Row; - public static synthetic fun copy-DTaUb5E$default (Lapp/cash/redwood/layout/Row;IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/cash/redwood/layout/Row; + public final fun component8 ()Lkotlin/jvm/functions/Function1; + public final fun copy-IkXdkrs (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/cash/redwood/layout/Row; + public static synthetic fun copy-IkXdkrs$default (Lapp/cash/redwood/layout/Row;IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/cash/redwood/layout/Row; public fun equals (Ljava/lang/Object;)Z public final fun getChildren ()Lkotlin/jvm/functions/Function1; public final fun getHeight-jfchsNQ ()I public final fun getHorizontalAlignment-Ng1ngeI ()I public final fun getMargin ()Lapp/cash/redwood/ui/Margin; + public final fun getOnScroll ()Lkotlin/jvm/functions/Function1; public final fun getOverflow-Andb1w4 ()I public final fun getVerticalAlignment-eoX4IBc ()I public final fun getWidth-jfchsNQ ()I diff --git a/redwood-layout-schema/redwood-api.xml b/redwood-layout-schema/redwood-api.xml index fac28ec74c..975f688d8d 100644 --- a/redwood-layout-schema/redwood-api.xml +++ b/redwood-layout-schema/redwood-api.xml @@ -6,6 +6,7 @@ + @@ -15,6 +16,7 @@ + diff --git a/redwood-layout-schema/src/main/kotlin/app/cash/redwood/layout/widgets.kt b/redwood-layout-schema/src/main/kotlin/app/cash/redwood/layout/widgets.kt index b46596aeef..3e26e4044b 100644 --- a/redwood-layout-schema/src/main/kotlin/app/cash/redwood/layout/widgets.kt +++ b/redwood-layout-schema/src/main/kotlin/app/cash/redwood/layout/widgets.kt @@ -80,6 +80,16 @@ public data class Row( @Default("CrossAxisAlignment.Start") val verticalAlignment: CrossAxisAlignment, + /** + * Invoked when the container scrolls. The function's `offset` is represented in units in the + * host's coordinate system. + * + * @see Overflow.Scroll + */ + @Property(7) + @Default("null") + val onScroll: ((offset: Double) -> Unit)?, + /** * A slot to add widgets in. */ @@ -142,6 +152,16 @@ public data class Column( @Default("MainAxisAlignment.Start") val verticalAlignment: MainAxisAlignment, + /** + * Invoked when the container scrolls. The function's `offset` is represented in units in the + * host's coordinate system. + * + * @see Overflow.Scroll + */ + @Property(7) + @Default("null") + val onScroll: ((offset: Double) -> Unit)?, + /** * A slot to add widgets in. */ diff --git a/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractFlexContainerTest.kt b/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractFlexContainerTest.kt index 1b31f3a7db..f5318f6370 100644 --- a/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractFlexContainerTest.kt +++ b/redwood-layout-shared-test/src/commonMain/kotlin/app/cash/redwood/layout/AbstractFlexContainerTest.kt @@ -19,6 +19,7 @@ import app.cash.redwood.Modifier import app.cash.redwood.layout.api.Constraint import app.cash.redwood.layout.api.CrossAxisAlignment import app.cash.redwood.layout.api.MainAxisAlignment +import app.cash.redwood.layout.api.Overflow import app.cash.redwood.layout.widget.Column import app.cash.redwood.layout.widget.Row import app.cash.redwood.ui.Margin @@ -27,6 +28,7 @@ import app.cash.redwood.widget.ChangeListener import app.cash.redwood.widget.Widget import app.cash.redwood.yoga.FlexDirection import kotlin.test.Test +import kotlin.test.assertTrue abstract class AbstractFlexContainerTest { abstract fun flexContainer( @@ -602,6 +604,25 @@ abstract class AbstractFlexContainerTest { container.children.detach() verifySnapshot(container, "After") } + + @Test + fun testOnScrollListener() { + var scrolled = false + val container = flexContainer(FlexDirection.Column).apply { + width(Constraint.Fill) + height(Constraint.Fill) + overflow(Overflow.Scroll) + onScroll { + scrolled = true + } + } + + container.scroll(1000.0) + + verifySnapshot(container) + + assertTrue(scrolled) + } } interface TestFlexContainer : @@ -614,6 +635,9 @@ interface TestFlexContainer : fun crossAxisAlignment(crossAxisAlignment: CrossAxisAlignment) fun mainAxisAlignment(mainAxisAlignment: MainAxisAlignment) fun margin(margin: Margin) + fun overflow(overflow: Overflow) + fun onScroll(onScroll: ((Double) -> Unit)?) + fun scroll(offset: Double) fun add(widget: Widget) fun addAt(index: Int, widget: Widget) fun removeAt(index: Int) diff --git a/redwood-layout-testing/api/redwood-layout-testing.api b/redwood-layout-testing/api/redwood-layout-testing.api index ad56af8656..552c30d263 100644 --- a/redwood-layout-testing/api/redwood-layout-testing.api +++ b/redwood-layout-testing/api/redwood-layout-testing.api @@ -17,8 +17,8 @@ public final class app/cash/redwood/layout/testing/BoxValue : app/cash/redwood/t } public final class app/cash/redwood/layout/testing/ColumnValue : app/cash/redwood/testing/WidgetValue { - public synthetic fun (Lapp/cash/redwood/Modifier;IILapp/cash/redwood/ui/Margin;IIILjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Lapp/cash/redwood/Modifier;IILapp/cash/redwood/ui/Margin;IIILjava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lapp/cash/redwood/Modifier;IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lapp/cash/redwood/Modifier;IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Ljava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public final fun getChildren ()Ljava/util/List; public fun getChildrenLists ()Ljava/util/List; @@ -26,6 +26,7 @@ public final class app/cash/redwood/layout/testing/ColumnValue : app/cash/redwoo public final fun getHorizontalAlignment-eoX4IBc ()I public final fun getMargin ()Lapp/cash/redwood/ui/Margin; public fun getModifier ()Lapp/cash/redwood/Modifier; + public final fun getOnScroll ()Lkotlin/jvm/functions/Function1; public final fun getOverflow-Andb1w4 ()I public final fun getVerticalAlignment-Ng1ngeI ()I public final fun getWidth-jfchsNQ ()I @@ -49,8 +50,8 @@ public final class app/cash/redwood/layout/testing/RedwoodLayoutTestingWidgetFac } public final class app/cash/redwood/layout/testing/RowValue : app/cash/redwood/testing/WidgetValue { - public synthetic fun (Lapp/cash/redwood/Modifier;IILapp/cash/redwood/ui/Margin;IIILjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Lapp/cash/redwood/Modifier;IILapp/cash/redwood/ui/Margin;IIILjava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lapp/cash/redwood/Modifier;IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lapp/cash/redwood/Modifier;IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Ljava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public final fun getChildren ()Ljava/util/List; public fun getChildrenLists ()Ljava/util/List; @@ -58,6 +59,7 @@ public final class app/cash/redwood/layout/testing/RowValue : app/cash/redwood/t public final fun getHorizontalAlignment-Ng1ngeI ()I public final fun getMargin ()Lapp/cash/redwood/ui/Margin; public fun getModifier ()Lapp/cash/redwood/Modifier; + public final fun getOnScroll ()Lkotlin/jvm/functions/Function1; public final fun getOverflow-Andb1w4 ()I public final fun getVerticalAlignment-eoX4IBc ()I public final fun getWidth-jfchsNQ ()I diff --git a/redwood-layout-testing/api/redwood-layout-testing.klib.api b/redwood-layout-testing/api/redwood-layout-testing.klib.api index 22acaaa2fb..05f1c51b76 100644 --- a/redwood-layout-testing/api/redwood-layout-testing.klib.api +++ b/redwood-layout-testing/api/redwood-layout-testing.klib.api @@ -34,7 +34,7 @@ final class app.cash.redwood.layout.testing/BoxValue : app.cash.redwood.testing/ } final class app.cash.redwood.layout.testing/ColumnValue : app.cash.redwood.testing/WidgetValue { // app.cash.redwood.layout.testing/ColumnValue|null[0] - constructor (app.cash.redwood/Modifier = ..., app.cash.redwood.layout.api/Constraint = ..., app.cash.redwood.layout.api/Constraint = ..., app.cash.redwood.ui/Margin = ..., app.cash.redwood.layout.api/Overflow = ..., app.cash.redwood.layout.api/CrossAxisAlignment = ..., app.cash.redwood.layout.api/MainAxisAlignment = ..., kotlin.collections/List = ...) // app.cash.redwood.layout.testing/ColumnValue.|(app.cash.redwood.Modifier;app.cash.redwood.layout.api.Constraint;app.cash.redwood.layout.api.Constraint;app.cash.redwood.ui.Margin;app.cash.redwood.layout.api.Overflow;app.cash.redwood.layout.api.CrossAxisAlignment;app.cash.redwood.layout.api.MainAxisAlignment;kotlin.collections.List){}[0] + constructor (app.cash.redwood/Modifier = ..., app.cash.redwood.layout.api/Constraint = ..., app.cash.redwood.layout.api/Constraint = ..., app.cash.redwood.ui/Margin = ..., app.cash.redwood.layout.api/Overflow = ..., app.cash.redwood.layout.api/CrossAxisAlignment = ..., app.cash.redwood.layout.api/MainAxisAlignment = ..., kotlin/Function1? = ..., kotlin.collections/List = ...) // app.cash.redwood.layout.testing/ColumnValue.|(app.cash.redwood.Modifier;app.cash.redwood.layout.api.Constraint;app.cash.redwood.layout.api.Constraint;app.cash.redwood.ui.Margin;app.cash.redwood.layout.api.Overflow;app.cash.redwood.layout.api.CrossAxisAlignment;app.cash.redwood.layout.api.MainAxisAlignment;kotlin.Function1?;kotlin.collections.List){}[0] final val children // app.cash.redwood.layout.testing/ColumnValue.children|{}children[0] final fun (): kotlin.collections/List // app.cash.redwood.layout.testing/ColumnValue.children.|(){}[0] @@ -48,6 +48,8 @@ final class app.cash.redwood.layout.testing/ColumnValue : app.cash.redwood.testi final fun (): app.cash.redwood.ui/Margin // app.cash.redwood.layout.testing/ColumnValue.margin.|(){}[0] final val modifier // app.cash.redwood.layout.testing/ColumnValue.modifier|{}modifier[0] final fun (): app.cash.redwood/Modifier // app.cash.redwood.layout.testing/ColumnValue.modifier.|(){}[0] + final val onScroll // app.cash.redwood.layout.testing/ColumnValue.onScroll|{}onScroll[0] + final fun (): kotlin/Function1? // app.cash.redwood.layout.testing/ColumnValue.onScroll.|(){}[0] final val overflow // app.cash.redwood.layout.testing/ColumnValue.overflow|{}overflow[0] final fun (): app.cash.redwood.layout.api/Overflow // app.cash.redwood.layout.testing/ColumnValue.overflow.|(){}[0] final val verticalAlignment // app.cash.redwood.layout.testing/ColumnValue.verticalAlignment|{}verticalAlignment[0] @@ -72,7 +74,7 @@ final class app.cash.redwood.layout.testing/RedwoodLayoutTestingWidgetFactory : } final class app.cash.redwood.layout.testing/RowValue : app.cash.redwood.testing/WidgetValue { // app.cash.redwood.layout.testing/RowValue|null[0] - constructor (app.cash.redwood/Modifier = ..., app.cash.redwood.layout.api/Constraint = ..., app.cash.redwood.layout.api/Constraint = ..., app.cash.redwood.ui/Margin = ..., app.cash.redwood.layout.api/Overflow = ..., app.cash.redwood.layout.api/MainAxisAlignment = ..., app.cash.redwood.layout.api/CrossAxisAlignment = ..., kotlin.collections/List = ...) // app.cash.redwood.layout.testing/RowValue.|(app.cash.redwood.Modifier;app.cash.redwood.layout.api.Constraint;app.cash.redwood.layout.api.Constraint;app.cash.redwood.ui.Margin;app.cash.redwood.layout.api.Overflow;app.cash.redwood.layout.api.MainAxisAlignment;app.cash.redwood.layout.api.CrossAxisAlignment;kotlin.collections.List){}[0] + constructor (app.cash.redwood/Modifier = ..., app.cash.redwood.layout.api/Constraint = ..., app.cash.redwood.layout.api/Constraint = ..., app.cash.redwood.ui/Margin = ..., app.cash.redwood.layout.api/Overflow = ..., app.cash.redwood.layout.api/MainAxisAlignment = ..., app.cash.redwood.layout.api/CrossAxisAlignment = ..., kotlin/Function1? = ..., kotlin.collections/List = ...) // app.cash.redwood.layout.testing/RowValue.|(app.cash.redwood.Modifier;app.cash.redwood.layout.api.Constraint;app.cash.redwood.layout.api.Constraint;app.cash.redwood.ui.Margin;app.cash.redwood.layout.api.Overflow;app.cash.redwood.layout.api.MainAxisAlignment;app.cash.redwood.layout.api.CrossAxisAlignment;kotlin.Function1?;kotlin.collections.List){}[0] final val children // app.cash.redwood.layout.testing/RowValue.children|{}children[0] final fun (): kotlin.collections/List // app.cash.redwood.layout.testing/RowValue.children.|(){}[0] @@ -86,6 +88,8 @@ final class app.cash.redwood.layout.testing/RowValue : app.cash.redwood.testing/ final fun (): app.cash.redwood.ui/Margin // app.cash.redwood.layout.testing/RowValue.margin.|(){}[0] final val modifier // app.cash.redwood.layout.testing/RowValue.modifier|{}modifier[0] final fun (): app.cash.redwood/Modifier // app.cash.redwood.layout.testing/RowValue.modifier.|(){}[0] + final val onScroll // app.cash.redwood.layout.testing/RowValue.onScroll|{}onScroll[0] + final fun (): kotlin/Function1? // app.cash.redwood.layout.testing/RowValue.onScroll.|(){}[0] final val overflow // app.cash.redwood.layout.testing/RowValue.overflow|{}overflow[0] final fun (): app.cash.redwood.layout.api/Overflow // app.cash.redwood.layout.testing/RowValue.overflow.|(){}[0] final val verticalAlignment // app.cash.redwood.layout.testing/RowValue.verticalAlignment|{}verticalAlignment[0] diff --git a/redwood-layout-uiview/RedwoodLayoutUIViewTests/__Snapshots__/UIViewFlexContainerTestHost/testOnScrollListener.1.png b/redwood-layout-uiview/RedwoodLayoutUIViewTests/__Snapshots__/UIViewFlexContainerTestHost/testOnScrollListener.1.png new file mode 100644 index 0000000000..825c896dc4 --- /dev/null +++ b/redwood-layout-uiview/RedwoodLayoutUIViewTests/__Snapshots__/UIViewFlexContainerTestHost/testOnScrollListener.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e63d8061520f28e90252d1d2473c594773c707b2156541981933840f5036207 +size 65306 diff --git a/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainer.kt b/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainer.kt index 98fc0eb9b1..2df23430fb 100644 --- a/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainer.kt +++ b/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainer.kt @@ -33,7 +33,7 @@ internal class UIViewFlexContainer( ) : YogaFlexContainer, ChangeListener { private val yogaView: YogaUIView = YogaUIView( - applyModifier = { node, index -> + applyModifier = { node, _ -> node.applyModifier(node.context as Modifier, Density.Default) }, ) @@ -74,6 +74,10 @@ internal class UIViewFlexContainer( yogaView.scrollEnabled = overflow == Overflow.Scroll } + override fun onScroll(onScroll: ((Double) -> Unit)?) { + yogaView.onScroll = onScroll + } + override fun onEndChanges() { value.invalidateIntrinsicContentSize() // Tell the enclosing view that our size changed. value.setNeedsLayout() // Update layout of subviews. diff --git a/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/YogaUIView.kt b/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/YogaUIView.kt index c1d8954fb4..d86da3ab99 100644 --- a/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/YogaUIView.kt +++ b/redwood-layout-uiview/src/commonMain/kotlin/app/cash/redwood/layout/uiview/YogaUIView.kt @@ -19,17 +19,20 @@ import platform.CoreGraphics.CGSize import platform.CoreGraphics.CGSizeMake import platform.UIKit.UIScrollView import platform.UIKit.UIScrollViewContentInsetAdjustmentBehavior.UIScrollViewContentInsetAdjustmentNever +import platform.UIKit.UIScrollViewDelegateProtocol import platform.UIKit.UIView import platform.UIKit.UIViewNoIntrinsicMetric internal class YogaUIView( private val applyModifier: (Node, Int) -> Unit, -) : UIScrollView(cValue { CGRectZero }) { +) : UIScrollView(cValue { CGRectZero }), UIScrollViewDelegateProtocol { val rootNode = Node() var width = Constraint.Wrap var height = Constraint.Wrap + var onScroll: ((Double) -> Unit)? = null + init { // TODO: Support scroll indicators. scrollEnabled = false @@ -62,7 +65,7 @@ internal class YogaUIView( // This duplicates the calculation we're doing above, and should be // combined into one call. val scrollSize = bounds.useContents { - if (rootNode.flexDirection == FlexDirection.Column) { + if (isColumn()) { CGSizeMake(width, Size.UNDEFINED.toDouble()) } else { CGSizeMake(Size.UNDEFINED.toDouble(), height) @@ -128,6 +131,8 @@ internal class YogaUIView( } override fun setScrollEnabled(scrollEnabled: Boolean) { + delegate = if (scrollEnabled) this else null + val previousScrollEnabled = this.scrollEnabled super.setScrollEnabled(scrollEnabled) @@ -136,6 +141,20 @@ internal class YogaUIView( setNeedsLayout() } } + + override fun scrollViewDidScroll(scrollView: UIScrollView) { + val onScroll = onScroll + if (onScroll != null) { + val offset = scrollView.contentOffset.useContents { + if (isColumn()) y else x + } + onScroll(offset) + } + } + + private fun isColumn(): Boolean { + return rootNode.flexDirection == FlexDirection.Column + } } private val Node.view: UIView diff --git a/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainerTest.kt b/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainerTest.kt index 9ace70ad40..109fa5dcbc 100644 --- a/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainerTest.kt +++ b/redwood-layout-uiview/src/commonTest/kotlin/app/cash/redwood/layout/uiview/UIViewFlexContainerTest.kt @@ -22,8 +22,10 @@ import app.cash.redwood.layout.widget.FlexContainer import app.cash.redwood.widget.ChangeListener import app.cash.redwood.widget.Widget import app.cash.redwood.yoga.FlexDirection +import kotlinx.cinterop.cValue import platform.CoreGraphics.CGRectMake import platform.UIKit.UIColor +import platform.UIKit.UIScrollView import platform.UIKit.UIView class UIViewFlexContainerTest( @@ -50,12 +52,21 @@ class UIViewFlexContainerTest( FlexContainer by delegate, ChangeListener by delegate { private var childCount = 0 + override val children: Widget.Children = delegate.children init { value.backgroundColor = UIColor(red = 0.0, green = 0.0, blue = 1.0, alpha = 0.2) } + override fun onScroll(onScroll: ((Double) -> Unit)?) { + delegate.onScroll(onScroll) + } + + override fun scroll(offset: Double) { + (delegate.value as UIScrollView).setContentOffset(cValue { y = offset }, false) + } + override fun add(widget: Widget) { addAt(childCount, widget) } diff --git a/redwood-layout-view/src/main/kotlin/app/cash/redwood/layout/view/ViewFlexContainer.kt b/redwood-layout-view/src/main/kotlin/app/cash/redwood/layout/view/ViewFlexContainer.kt index d267eba0cc..c91326deb2 100644 --- a/redwood-layout-view/src/main/kotlin/app/cash/redwood/layout/view/ViewFlexContainer.kt +++ b/redwood-layout-view/src/main/kotlin/app/cash/redwood/layout/view/ViewFlexContainer.kt @@ -16,6 +16,7 @@ package app.cash.redwood.layout.view import android.content.Context +import android.os.Build.VERSION.SDK_INT import android.util.LayoutDirection import android.view.View import android.view.ViewGroup @@ -25,6 +26,7 @@ import android.widget.FrameLayout import android.widget.HorizontalScrollView import androidx.core.view.updateLayoutParams import androidx.core.widget.NestedScrollView +import androidx.core.widget.NestedScrollView.OnScrollChangeListener as OnScrollChangeListenerCompat import app.cash.redwood.Modifier import app.cash.redwood.layout.api.Constraint import app.cash.redwood.layout.api.Overflow @@ -65,6 +67,8 @@ internal class ViewFlexContainer( }, ) + private var onScroll: ((Double) -> Unit)? = null + override var modifier: Modifier = Modifier init { @@ -96,6 +100,11 @@ internal class ViewFlexContainer( } } + override fun onScroll(onScroll: ((Double) -> Unit)?) { + this.onScroll = onScroll + hostView.attachOrDetachScrollListeners() + } + override fun onEndChanges() { hostView.invalidate() hostView.requestLayout() @@ -113,17 +122,36 @@ internal class ViewFlexContainer( } } + // Either OnScrollChangeListenerCompat or OnScrollChangeListener. Created lazily. + private var onScrollListener: Any? = null + init { layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT) updateViewHierarchy() } + fun attachOrDetachScrollListeners() { + val child = getChildAt(0) + if (child is NestedScrollView) { + val listener = (onScrollListener as OnScrollChangeListenerCompat?) + ?: OnScrollChangeListenerCompat { _, _, scrollY, _, _ -> onScroll?.invoke(scrollY.toDouble()) } + .also { onScrollListener = it } + child.setOnScrollChangeListener(listener) + } else if (SDK_INT >= 23 && child is HorizontalScrollView) { + val listener = (onScrollListener as OnScrollChangeListener?) + ?: OnScrollChangeListener { _, scrollX, _, _, _ -> onScroll?.invoke(scrollX.toDouble()) } + .also { onScrollListener = it } + child.setOnScrollChangeListener(listener) + } + } + private fun updateViewHierarchy() { removeAllViews() (yogaLayout.parent as ViewGroup?)?.removeView(yogaLayout) if (scrollEnabled) { addView(newScrollView().apply { addView(yogaLayout) }) + attachOrDetachScrollListeners() } else { addView(yogaLayout) } diff --git a/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewFlexContainerTest.kt b/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewFlexContainerTest.kt index 6423c38057..d7ae8e8381 100644 --- a/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewFlexContainerTest.kt +++ b/redwood-layout-view/src/test/kotlin/app/cash/redwood/layout/view/ViewFlexContainerTest.kt @@ -69,7 +69,18 @@ class ViewFlexContainerTest( FlexContainer by delegate, ChangeListener by delegate { private var childCount = 0 + private var onScroll: ((Double) -> Unit)? = null + override val children: ViewGroupChildren = delegate.children + + override fun onScroll(onScroll: ((Double) -> Unit)?) { + this.onScroll = onScroll + } + + override fun scroll(offset: Double) { + onScroll?.invoke(offset) + } + override fun add(widget: Widget) { addAt(childCount, widget) } diff --git a/redwood-layout-view/src/test/snapshots/images/app.cash.redwood.layout.view_ViewFlexContainerTest_testOnScrollListener[LTR].png b/redwood-layout-view/src/test/snapshots/images/app.cash.redwood.layout.view_ViewFlexContainerTest_testOnScrollListener[LTR].png new file mode 100644 index 0000000000..ebb6a426ef --- /dev/null +++ b/redwood-layout-view/src/test/snapshots/images/app.cash.redwood.layout.view_ViewFlexContainerTest_testOnScrollListener[LTR].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdca08973fd5cbf55a12717a65eaa147bbfaff662cc7dca62bcae0f9d63a9859 +size 3837 diff --git a/redwood-layout-view/src/test/snapshots/images/app.cash.redwood.layout.view_ViewFlexContainerTest_testOnScrollListener[RTL].png b/redwood-layout-view/src/test/snapshots/images/app.cash.redwood.layout.view_ViewFlexContainerTest_testOnScrollListener[RTL].png new file mode 100644 index 0000000000..ebb6a426ef --- /dev/null +++ b/redwood-layout-view/src/test/snapshots/images/app.cash.redwood.layout.view_ViewFlexContainerTest_testOnScrollListener[RTL].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdca08973fd5cbf55a12717a65eaa147bbfaff662cc7dca62bcae0f9d63a9859 +size 3837 diff --git a/redwood-layout-widget/api/redwood-layout-widget.api b/redwood-layout-widget/api/redwood-layout-widget.api index 36b83e1f29..d6a8190ea4 100644 --- a/redwood-layout-widget/api/redwood-layout-widget.api +++ b/redwood-layout-widget/api/redwood-layout-widget.api @@ -12,6 +12,7 @@ public abstract interface class app/cash/redwood/layout/widget/Column : app/cash public abstract fun height-DyLkt4w (I)V public abstract fun horizontalAlignment-njEs0f8 (I)V public abstract fun margin (Lapp/cash/redwood/ui/Margin;)V + public abstract fun onScroll (Lkotlin/jvm/functions/Function1;)V public abstract fun overflow-OcAJ2MY (I)V public abstract fun verticalAlignment-6exqka8 (I)V public abstract fun width-DyLkt4w (I)V @@ -53,6 +54,7 @@ public abstract interface class app/cash/redwood/layout/widget/Row : app/cash/re public abstract fun height-DyLkt4w (I)V public abstract fun horizontalAlignment-6exqka8 (I)V public abstract fun margin (Lapp/cash/redwood/ui/Margin;)V + public abstract fun onScroll (Lkotlin/jvm/functions/Function1;)V public abstract fun overflow-OcAJ2MY (I)V public abstract fun verticalAlignment-njEs0f8 (I)V public abstract fun width-DyLkt4w (I)V diff --git a/redwood-layout-widget/api/redwood-layout-widget.klib.api b/redwood-layout-widget/api/redwood-layout-widget.klib.api index d016656f36..872da7198f 100644 --- a/redwood-layout-widget/api/redwood-layout-widget.klib.api +++ b/redwood-layout-widget/api/redwood-layout-widget.klib.api @@ -24,6 +24,7 @@ abstract interface <#A: kotlin/Any> app.cash.redwood.layout.widget/Column : app. abstract fun height(app.cash.redwood.layout.api/Constraint) // app.cash.redwood.layout.widget/Column.height|height(app.cash.redwood.layout.api.Constraint){}[0] abstract fun horizontalAlignment(app.cash.redwood.layout.api/CrossAxisAlignment) // app.cash.redwood.layout.widget/Column.horizontalAlignment|horizontalAlignment(app.cash.redwood.layout.api.CrossAxisAlignment){}[0] abstract fun margin(app.cash.redwood.ui/Margin) // app.cash.redwood.layout.widget/Column.margin|margin(app.cash.redwood.ui.Margin){}[0] + abstract fun onScroll(kotlin/Function1?) // app.cash.redwood.layout.widget/Column.onScroll|onScroll(kotlin.Function1?){}[0] abstract fun overflow(app.cash.redwood.layout.api/Overflow) // app.cash.redwood.layout.widget/Column.overflow|overflow(app.cash.redwood.layout.api.Overflow){}[0] abstract fun verticalAlignment(app.cash.redwood.layout.api/MainAxisAlignment) // app.cash.redwood.layout.widget/Column.verticalAlignment|verticalAlignment(app.cash.redwood.layout.api.MainAxisAlignment){}[0] abstract fun width(app.cash.redwood.layout.api/Constraint) // app.cash.redwood.layout.widget/Column.width|width(app.cash.redwood.layout.api.Constraint){}[0] @@ -61,6 +62,7 @@ abstract interface <#A: kotlin/Any> app.cash.redwood.layout.widget/Row : app.cas abstract fun height(app.cash.redwood.layout.api/Constraint) // app.cash.redwood.layout.widget/Row.height|height(app.cash.redwood.layout.api.Constraint){}[0] abstract fun horizontalAlignment(app.cash.redwood.layout.api/MainAxisAlignment) // app.cash.redwood.layout.widget/Row.horizontalAlignment|horizontalAlignment(app.cash.redwood.layout.api.MainAxisAlignment){}[0] abstract fun margin(app.cash.redwood.ui/Margin) // app.cash.redwood.layout.widget/Row.margin|margin(app.cash.redwood.ui.Margin){}[0] + abstract fun onScroll(kotlin/Function1?) // app.cash.redwood.layout.widget/Row.onScroll|onScroll(kotlin.Function1?){}[0] abstract fun overflow(app.cash.redwood.layout.api/Overflow) // app.cash.redwood.layout.widget/Row.overflow|overflow(app.cash.redwood.layout.api.Overflow){}[0] abstract fun verticalAlignment(app.cash.redwood.layout.api/CrossAxisAlignment) // app.cash.redwood.layout.widget/Row.verticalAlignment|verticalAlignment(app.cash.redwood.layout.api.CrossAxisAlignment){}[0] abstract fun width(app.cash.redwood.layout.api/Constraint) // app.cash.redwood.layout.widget/Row.width|width(app.cash.redwood.layout.api.Constraint){}[0] diff --git a/redwood-lazylayout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiLazyListTest.kt b/redwood-lazylayout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiLazyListTest.kt index 0b7e0a7d38..d62da865df 100644 --- a/redwood-lazylayout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiLazyListTest.kt +++ b/redwood-lazylayout-composeui/src/androidUnitTest/kotlin/app/cash/redwood/layout/composeui/ComposeUiLazyListTest.kt @@ -33,6 +33,7 @@ import app.cash.redwood.layout.TestFlexContainer import app.cash.redwood.layout.Text import app.cash.redwood.layout.Transparent import app.cash.redwood.layout.api.MainAxisAlignment +import app.cash.redwood.layout.api.Overflow import app.cash.redwood.layout.widget.Column import app.cash.redwood.layout.widget.Row import app.cash.redwood.lazylayout.composeui.ComposeUiLazyList @@ -111,6 +112,7 @@ class ComposeUiLazyListTest( override val value: @Composable () -> Unit get() = delegate.value private var childCount = 0 + private var onScroll: ((Double) -> Unit)? = null constructor(direction: FlexDirection, backgroundColor: Int) : this( ComposeUiLazyList().apply { @@ -124,6 +126,17 @@ class ComposeUiLazyListTest( override fun mainAxisAlignment(mainAxisAlignment: MainAxisAlignment) { } + override fun onScroll(onScroll: ((Double) -> Unit)?) { + this.onScroll = onScroll + } + + override fun scroll(offset: Double) { + onScroll?.invoke(offset) + } + + override fun overflow(overflow: Overflow) { + } + override fun add(widget: Widget<@Composable () -> Unit>) { addAt(childCount, widget) } diff --git a/redwood-lazylayout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiLazyListTest_testOnScrollListener[LTR].png b/redwood-lazylayout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiLazyListTest_testOnScrollListener[LTR].png new file mode 100644 index 0000000000..ebb6a426ef --- /dev/null +++ b/redwood-lazylayout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiLazyListTest_testOnScrollListener[LTR].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdca08973fd5cbf55a12717a65eaa147bbfaff662cc7dca62bcae0f9d63a9859 +size 3837 diff --git a/redwood-lazylayout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiLazyListTest_testOnScrollListener[RTL].png b/redwood-lazylayout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiLazyListTest_testOnScrollListener[RTL].png new file mode 100644 index 0000000000..ebb6a426ef --- /dev/null +++ b/redwood-lazylayout-composeui/src/test/snapshots/images/app.cash.redwood.layout.composeui_ComposeUiLazyListTest_testOnScrollListener[RTL].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdca08973fd5cbf55a12717a65eaa147bbfaff662cc7dca62bcae0f9d63a9859 +size 3837 diff --git a/redwood-lazylayout-view/src/test/kotlin/app/cash/redwood/lazylayout/view/ViewLazyListTest.kt b/redwood-lazylayout-view/src/test/kotlin/app/cash/redwood/lazylayout/view/ViewLazyListTest.kt index bd8cdc229a..e7294b7f34 100644 --- a/redwood-lazylayout-view/src/test/kotlin/app/cash/redwood/lazylayout/view/ViewLazyListTest.kt +++ b/redwood-lazylayout-view/src/test/kotlin/app/cash/redwood/lazylayout/view/ViewLazyListTest.kt @@ -26,6 +26,7 @@ import app.cash.redwood.layout.AbstractFlexContainerTest import app.cash.redwood.layout.TestFlexContainer import app.cash.redwood.layout.Text import app.cash.redwood.layout.api.MainAxisAlignment +import app.cash.redwood.layout.api.Overflow import app.cash.redwood.layout.view.ViewRedwoodLayoutWidgetFactory import app.cash.redwood.layout.widget.Column import app.cash.redwood.layout.widget.Row @@ -93,6 +94,7 @@ class ViewLazyListTest( LazyList by delegate, ChangeListener by delegate { private var childCount = 0 + private var onScroll: ((Double) -> Unit)? = null constructor(context: Context, direction: FlexDirection, backgroundColor: Int) : this( ViewLazyList(context).apply { @@ -103,9 +105,20 @@ class ViewLazyListTest( override val children: Widget.Children = delegate.items + override fun onScroll(onScroll: ((Double) -> Unit)?) { + this.onScroll = onScroll + } + + override fun scroll(offset: Double) { + onScroll?.invoke(offset) + } + override fun mainAxisAlignment(mainAxisAlignment: MainAxisAlignment) { } + override fun overflow(overflow: Overflow) { + } + override fun add(widget: Widget) { addAt(childCount, widget) } diff --git a/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testOnScrollListener[LTR].png b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testOnScrollListener[LTR].png new file mode 100644 index 0000000000..ebb6a426ef --- /dev/null +++ b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testOnScrollListener[LTR].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdca08973fd5cbf55a12717a65eaa147bbfaff662cc7dca62bcae0f9d63a9859 +size 3837 diff --git a/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testOnScrollListener[RTL].png b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testOnScrollListener[RTL].png new file mode 100644 index 0000000000..ebb6a426ef --- /dev/null +++ b/redwood-lazylayout-view/src/test/snapshots/images/app.cash.redwood.lazylayout.view_ViewLazyListTest_testOnScrollListener[RTL].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdca08973fd5cbf55a12717a65eaa147bbfaff662cc7dca62bcae0f9d63a9859 +size 3837 diff --git a/samples/emoji-search/presenter/src/commonMain/kotlin/com/example/redwood/emojisearch/presenter/EmojiSearch.kt b/samples/emoji-search/presenter/src/commonMain/kotlin/com/example/redwood/emojisearch/presenter/EmojiSearch.kt index 1b4237f270..f1f2a8cda9 100644 --- a/samples/emoji-search/presenter/src/commonMain/kotlin/com/example/redwood/emojisearch/presenter/EmojiSearch.kt +++ b/samples/emoji-search/presenter/src/commonMain/kotlin/com/example/redwood/emojisearch/presenter/EmojiSearch.kt @@ -186,7 +186,8 @@ fun Item( Image( url = emojiImage.url, modifier = Modifier - .margin(Margin(8.dp)), + .margin(Margin(8.dp)) + .size(24.dp, 24.dp), onClick = onClick, ) Text(text = emojiImage.label)