Merge "Fix CJK composition bug in BasicTextField2" into androidx-main
diff --git a/busytown/androidx.sh b/busytown/androidx.sh
index 19e56cd..20ea0f3 100755
--- a/busytown/androidx.sh
+++ b/busytown/androidx.sh
@@ -23,12 +23,11 @@
-Pandroidx.enableComposeCompilerMetrics=true \
-Pandroidx.enableComposeCompilerReports=true \
-Pandroidx.constraints=true \
- --no-daemon \
- --profile "$@"; then
+ --no-daemon "$@"; then
EXIT_VALUE=1
fi
- # Parse performance profile reports (generated with the --profile option above) and re-export
+ # Parse performance profile reports (generated with the --profile option) and re-export
# the metrics in an easily machine-readable format for tracking
impl/parse_profile_data.sh
fi
diff --git a/busytown/androidx_incremental.sh b/busytown/androidx_incremental.sh
index 775d422..949b04b 100755
--- a/busytown/androidx_incremental.sh
+++ b/busytown/androidx_incremental.sh
@@ -64,7 +64,6 @@
else
# Run Gradle
if impl/build.sh $DIAGNOSE_ARG buildOnServer checkExternalLicenses listTaskOutputs exportSboms \
- --profile \
"$@"; then
echo build succeeded
EXIT_VALUE=0
@@ -73,7 +72,7 @@
EXIT_VALUE=1
fi
- # Parse performance profile reports (generated with the --profile option above) and re-export the metrics in an easily machine-readable format for tracking
+ # Parse performance profile reports (generated with the --profile option) and re-export the metrics in an easily machine-readable format for tracking
impl/parse_profile_data.sh
fi
diff --git a/busytown/impl/build-studio-and-androidx.sh b/busytown/impl/build-studio-and-androidx.sh
index d37dd88..1511b7c 100755
--- a/busytown/impl/build-studio-and-androidx.sh
+++ b/busytown/impl/build-studio-and-androidx.sh
@@ -99,5 +99,5 @@
export USE_ANDROIDX_REMOTE_BUILD_CACHE=gcp
fi
-$SCRIPTS_DIR/impl/build.sh $androidxArguments --profile --dependency-verification=off -Pandroidx.validateNoUnrecognizedMessages=false
+$SCRIPTS_DIR/impl/build.sh $androidxArguments --dependency-verification=off -Pandroidx.validateNoUnrecognizedMessages=false
echo "Completing $0 at $(date)"
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index 6d83e9f..d524379 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -56,6 +56,7 @@
import androidx.camera.core.ExposureState;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.Logger;
+import androidx.camera.core.PhysicalCameraInfo;
import androidx.camera.core.ZoomState;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraInfoInternal;
@@ -125,6 +126,9 @@
@NonNull
private final CameraManagerCompat mCameraManager;
+ @Nullable
+ private Set<PhysicalCameraInfo> mPhysicalCameraInfos;
+
/**
* Constructs an instance. Before {@link #linkWithCameraControl(Camera2CameraControlImpl)} is
* called, camera control related API (torch/exposure/zoom) will return default values.
@@ -627,6 +631,32 @@
return map;
}
+ @NonNull
+ @Override
+ public Set<PhysicalCameraInfo> getPhysicalCameraInfos() {
+ if (mPhysicalCameraInfos == null) {
+ mPhysicalCameraInfos = new HashSet<>();
+ for (String physicalCameraId : mCameraCharacteristicsCompat.getPhysicalCameraIds()) {
+ CameraCharacteristicsCompat characteristicsCompat;
+ try {
+ characteristicsCompat =
+ mCameraManager.getCameraCharacteristicsCompat(physicalCameraId);
+ } catch (CameraAccessExceptionCompat e) {
+ Logger.e(TAG,
+ "Failed to get CameraCharacteristics for cameraId " + physicalCameraId,
+ e);
+ return Collections.emptySet();
+ }
+
+ PhysicalCameraInfo physicalCameraInfo = Camera2PhysicalCameraInfo.of(
+ physicalCameraId, characteristicsCompat);
+ mPhysicalCameraInfos.add(physicalCameraInfo);
+ }
+ }
+
+ return mPhysicalCameraInfos;
+ }
+
/**
* A {@link LiveData} which can be redirected to another {@link LiveData}. If no redirection
* is set, initial value will be used.
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PhysicalCameraInfo.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PhysicalCameraInfo.java
new file mode 100644
index 0000000..2bf301e
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2PhysicalCameraInfo.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * 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 androidx.camera.camera2.internal;
+
+import android.hardware.camera2.CameraCharacteristics;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.core.PhysicalCameraInfo;
+import androidx.core.util.Preconditions;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Camera2 implementation of {@link PhysicalCameraInfo} which wraps physical camera id and
+ * camera characteristics.
+ */
+@RequiresApi(21)
+@AutoValue
+abstract class Camera2PhysicalCameraInfo implements PhysicalCameraInfo {
+
+ @NonNull
+ @Override
+ public abstract String getPhysicalCameraId();
+
+ @NonNull
+ public abstract CameraCharacteristicsCompat getCameraCharacteristicsCompat();
+
+ @RequiresApi(28)
+ @NonNull
+ @Override
+ public Integer getLensPoseReference() {
+ Integer lensPoseRef =
+ getCameraCharacteristicsCompat().get(CameraCharacteristics.LENS_POSE_REFERENCE);
+ Preconditions.checkNotNull(lensPoseRef);
+ return lensPoseRef;
+ }
+
+ /**
+ * Creates {@link Camera2PhysicalCameraInfo} instance.
+ *
+ * @param physicalCameraId physical camera id.
+ * @param cameraCharacteristicsCompat {@link CameraCharacteristicsCompat}.
+ * @return
+ */
+ @NonNull
+ public static Camera2PhysicalCameraInfo of(
+ @NonNull String physicalCameraId,
+ @NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat) {
+ return new AutoValue_Camera2PhysicalCameraInfo(
+ physicalCameraId, cameraCharacteristicsCompat);
+ }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index 1932f6a..a47d41c 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -62,6 +62,7 @@
import androidx.camera.core.DynamicRange;
import androidx.camera.core.ExposureState;
import androidx.camera.core.FocusMeteringAction;
+import androidx.camera.core.PhysicalCameraInfo;
import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
import androidx.camera.core.TorchState;
import androidx.camera.core.ZoomState;
@@ -86,6 +87,7 @@
import org.robolectric.shadows.StreamConfigurationMapBuilder;
import org.robolectric.util.ReflectionHelpers;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@@ -515,6 +517,34 @@
assertThat(map.get("3")).isSameInstanceAs(characteristicsPhysical3);
}
+ @Config(minSdk = 28)
+ @RequiresApi(28)
+ @Test
+ public void canReturnPhysicalCameraInfos()
+ throws CameraAccessExceptionCompat {
+ init(/* hasAvailableCapabilities = */ true);
+
+ CameraCharacteristics characteristics0 = mock(CameraCharacteristics.class);
+ CameraCharacteristics characteristicsPhysical2 = mock(CameraCharacteristics.class);
+ CameraCharacteristics characteristicsPhysical3 = mock(CameraCharacteristics.class);
+ when(characteristics0.getPhysicalCameraIds())
+ .thenReturn(new HashSet<>(Arrays.asList("0", "2", "3")));
+ CameraManagerCompat cameraManagerCompat = initCameraManagerWithPhysicalIds(
+ Arrays.asList(
+ new Pair<>("0", characteristics0),
+ new Pair<>("2", characteristicsPhysical2),
+ new Pair<>("3", characteristicsPhysical3)));
+ Camera2CameraInfoImpl impl = new Camera2CameraInfoImpl("0", cameraManagerCompat);
+
+ List<PhysicalCameraInfo> physicalCameraInfos = new ArrayList<>(
+ impl.getPhysicalCameraInfos());
+ assertThat(physicalCameraInfos.size()).isEqualTo(3);
+ assertThat(characteristics0.getPhysicalCameraIds()).containsExactly(
+ physicalCameraInfos.get(0).getPhysicalCameraId(),
+ physicalCameraInfos.get(1).getPhysicalCameraId(),
+ physicalCameraInfos.get(2).getPhysicalCameraId());
+ }
+
@Config(maxSdk = 27)
@Test
public void canReturnCameraCharacteristicsMapWithMainCamera()
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index 7d71a4f..1ae1ec4 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -405,6 +405,17 @@
Collections.singleton(DynamicRange.SDR));
}
+ /**
+ * Returns a set of {@link PhysicalCameraInfo}.
+ *
+ * @return Set of {@link PhysicalCameraInfo}.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ default Set<PhysicalCameraInfo> getPhysicalCameraInfos() {
+ return Collections.emptySet();
+ }
+
@StringDef(open = true, value = {IMPLEMENTATION_TYPE_UNKNOWN,
IMPLEMENTATION_TYPE_CAMERA2_LEGACY, IMPLEMENTATION_TYPE_CAMERA2,
IMPLEMENTATION_TYPE_FAKE})
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
index 240375f..ca80c82 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
@@ -66,10 +66,16 @@
public static final CameraSelector DEFAULT_BACK_CAMERA =
new CameraSelector.Builder().requireLensFacing(LENS_FACING_BACK).build();
- private LinkedHashSet<CameraFilter> mCameraFilterSet;
+ @NonNull
+ private final LinkedHashSet<CameraFilter> mCameraFilterSet;
- CameraSelector(LinkedHashSet<CameraFilter> cameraFilterSet) {
+ @Nullable
+ private final String mPhysicalCameraId;
+
+ CameraSelector(@NonNull LinkedHashSet<CameraFilter> cameraFilterSet,
+ @Nullable String physicalCameraId) {
mCameraFilterSet = cameraFilterSet;
+ mPhysicalCameraId = physicalCameraId;
}
/**
@@ -204,10 +210,25 @@
return currentLensFacing;
}
+ /**
+ * Returns the physical camera id.
+ *
+ * @return physical camera id.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public String getPhysicalCameraId() {
+ return mPhysicalCameraId;
+ }
+
/** Builder for a {@link CameraSelector}. */
public static final class Builder {
+ @NonNull
private final LinkedHashSet<CameraFilter> mCameraFilterSet;
+ @Nullable
+ private String mPhysicalCameraId;
+
public Builder() {
mCameraFilterSet = new LinkedHashSet<>();
}
@@ -270,10 +291,23 @@
return builder;
}
+ /**
+ * Sets the physical camera id.
+ *
+ * @param physicalCameraId physical camera id.
+ * @return this builder.
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public Builder setPhysicalCameraId(@NonNull String physicalCameraId) {
+ mPhysicalCameraId = physicalCameraId;
+ return this;
+ }
+
/** Builds the {@link CameraSelector}. */
@NonNull
public CameraSelector build() {
- return new CameraSelector(mCameraFilterSet);
+ return new CameraSelector(mCameraFilterSet, mPhysicalCameraId);
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
index ed8b596..346b571 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -392,7 +392,7 @@
sessionConfigBuilder.setExpectedFrameRateRange(streamSpec.getExpectedFrameRateRange());
- sessionConfigBuilder.addSurface(mDeferrableSurface, streamSpec.getDynamicRange());
+ sessionConfigBuilder.addSurface(mDeferrableSurface, streamSpec.getDynamicRange(), null);
sessionConfigBuilder.addErrorListener((sessionConfig, error) -> {
clearPipeline();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/PhysicalCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/PhysicalCameraInfo.java
new file mode 100644
index 0000000..1ab2035e
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/PhysicalCameraInfo.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * 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 androidx.camera.core;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ * An interface for retrieving physical camera information.
+ *
+ * <p>Applications can retrieve physical camera information via
+ * {@link CameraInfo#getPhysicalCameraInfos()}. As a comparison, {@link CameraInfo} represents
+ * logical camera information. A logical camera is a grouping of two or more of those physical
+ * cameras.
+ *
+ * <p>See <a href="https://developer.android.com/media/camera/camera2/multi-camera">Multi-camera API</a>
+ * for more information.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(21)
+public interface PhysicalCameraInfo {
+
+ /**
+ * Returns physical camera id.
+ *
+ * @return physical camera id.
+ */
+ @NonNull
+ String getPhysicalCameraId();
+
+ /**
+ * Returns {@link android.hardware.camera2.CameraCharacteristics#LENS_POSE_REFERENCE}.
+ *
+ * @return lens pose reference.
+ */
+ @RequiresApi(28)
+ @NonNull
+ Integer getLensPoseReference();
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 02d910b..77899224 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -332,7 +332,8 @@
// output target for these two cases.
if (mSurfaceProvider != null) {
sessionConfigBuilder.addSurface(mSessionDeferrableSurface,
- streamSpec.getDynamicRange());
+ streamSpec.getDynamicRange(),
+ getPhysicalCameraId());
}
sessionConfigBuilder.addErrorListener((sessionConfig, error) -> {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index bd07576..8bd0481 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -152,6 +152,9 @@
@Nullable
private CameraEffect mEffect;
+ @Nullable
+ private String mPhysicalCameraId;
+
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
////////////////////////////////////////////////////////////////////////////////////////////
@@ -362,6 +365,17 @@
}
}
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public void setPhysicalCameraId(@NonNull String physicalCameraId) {
+ mPhysicalCameraId = physicalCameraId;
+ }
+
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public String getPhysicalCameraId() {
+ return mPhysicalCameraId;
+ }
+
/**
* Updates the target rotation of the use case config.
*
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
index 7d101c05..1cd1453 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
@@ -28,6 +28,7 @@
import androidx.camera.core.ExperimentalZeroShutterLag;
import androidx.camera.core.ExposureState;
import androidx.camera.core.FocusMeteringAction;
+import androidx.camera.core.PhysicalCameraInfo;
import androidx.camera.core.ZoomState;
import androidx.lifecycle.LiveData;
@@ -228,4 +229,10 @@
public Object getPhysicalCameraCharacteristics(@NonNull String physicalCameraId) {
return mCameraInfoInternal.getPhysicalCameraCharacteristics(physicalCameraId);
}
+
+ @NonNull
+ @Override
+ public Set<PhysicalCameraInfo> getPhysicalCameraInfos() {
+ return mCameraInfoInternal.getPhysicalCameraInfos();
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
index 63bdf6d..414ddb0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SessionConfig.java
@@ -647,7 +647,7 @@
*/
@NonNull
public Builder addSurface(@NonNull DeferrableSurface surface) {
- return addSurface(surface, DynamicRange.SDR);
+ return addSurface(surface, DynamicRange.SDR, null);
}
/**
@@ -656,8 +656,10 @@
*/
@NonNull
public Builder addSurface(@NonNull DeferrableSurface surface,
- @NonNull DynamicRange dynamicRange) {
+ @NonNull DynamicRange dynamicRange,
+ @Nullable String physicalCameraId) {
OutputConfig outputConfig = OutputConfig.builder(surface)
+ .setPhysicalCameraId(physicalCameraId)
.setDynamicRange(dynamicRange)
.build();
mOutputConfigs.add(outputConfig);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
index 687a41b..9844499 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/streamsharing/StreamSharing.java
@@ -278,7 +278,7 @@
propagateChildrenCamera2Interop(streamSpec.getResolution(), builder);
- builder.addSurface(mCameraEdge.getDeferrableSurface(), streamSpec.getDynamicRange());
+ builder.addSurface(mCameraEdge.getDeferrableSurface(), streamSpec.getDynamicRange(), null);
builder.addRepeatingCameraCaptureCallback(
mVirtualCameraAdapter.getParentMetadataCallback());
if (streamSpec.getImplementationOptions() != null) {
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index aa18150..360497e 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -884,7 +884,7 @@
DynamicRange dynamicRange = streamSpec.getDynamicRange();
if (!isStreamError && mDeferrableSurface != null) {
if (isStreamActive) {
- sessionConfigBuilder.addSurface(mDeferrableSurface, dynamicRange);
+ sessionConfigBuilder.addSurface(mDeferrableSurface, dynamicRange, null);
} else {
sessionConfigBuilder.addNonRepeatingSurface(mDeferrableSurface, dynamicRange);
}
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
index 585c768..cf8436f 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Transition.kt
@@ -34,6 +34,7 @@
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateObserver
import androidx.compose.runtime.withFrameNanos
@@ -52,6 +53,7 @@
import kotlin.math.roundToLong
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -932,7 +934,7 @@
internal var startTimeNanos by mutableLongStateOf(AnimationConstants.UnspecifiedTime)
// This gets calculated every time child is updated/added
- internal var updateChildrenNeeded: Boolean by mutableStateOf(true)
+ private var updateChildrenNeeded: Boolean by mutableStateOf(false)
private val _animations = mutableStateListOf<TransitionAnimationState<*, *>>()
private val _transitions = mutableStateListOf<Transition<*>>()
@@ -1176,21 +1178,34 @@
internal fun animateTo(targetState: S) {
if (!isSeeking) {
updateTarget(targetState)
- // target != currentState adds LaunchedEffect into the tree in the same frame as
+ // target != currentState adds the effect into the tree in the same frame as
// target change.
if (targetState != currentState || isRunning || updateChildrenNeeded) {
- LaunchedEffect(this) {
- while (true) {
+ // We're using a composition-obtained scope + DisposableEffect here to give us
+ // control over coroutine dispatching
+ val coroutineScope = rememberCoroutineScope()
+ DisposableEffect(coroutineScope, this) {
+ // Launch the coroutine undispatched so the block is executed in the current
+ // frame. This is important as this initializes the state.
+ coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
val durationScale = coroutineContext.durationScale
withFrameNanos {
- // This check is very important, as isSeeking may be changed off-band
- // between the last check in composition and this callback which
- // happens in the animation callback the next frame.
if (!isSeeking) {
onFrame(it / AnimationDebugDurationScale, durationScale)
}
}
+ while (isRunning) {
+ withFrameNanos {
+ // This check is very important, as isSeeking may be changed
+ // off-band between the last check in composition and this callback
+ // which happens in the animation callback the next frame.
+ if (!isSeeking) {
+ onFrame(it / AnimationDebugDurationScale, durationScale)
+ }
+ }
+ }
}
+ onDispose { }
}
}
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index cdcea5f..62bdfbf 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -134,6 +134,7 @@
12200 to "1.7.0-alpha03",
12300 to "1.7.0-alpha04",
12400 to "1.7.0-alpha05",
+ 12500 to "1.7.0-alpha06",
)
/**
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 032cf78..a94e5bd 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -1748,9 +1748,13 @@
@androidx.compose.runtime.Stable public final class TextFieldState {
ctor public TextFieldState(optional String initialText, optional long initialSelection);
method public inline void edit(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.text.input.TextFieldBuffer,kotlin.Unit> block);
- method public androidx.compose.foundation.text.input.TextFieldCharSequence getText();
+ method public androidx.compose.ui.text.TextRange? getComposition();
+ method public long getSelection();
+ method public CharSequence getText();
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.text.input.UndoState getUndoState();
- property public final androidx.compose.foundation.text.input.TextFieldCharSequence text;
+ property public final androidx.compose.ui.text.TextRange? composition;
+ property public final long selection;
+ property public final CharSequence text;
property @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final androidx.compose.foundation.text.input.UndoState undoState;
}
@@ -1762,11 +1766,10 @@
public final class TextFieldStateKt {
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void clearText(androidx.compose.foundation.text.input.TextFieldState);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend Object? forEachTextValue(androidx.compose.foundation.text.input.TextFieldState, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.text.input.TextFieldCharSequence,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<?>);
method @androidx.compose.runtime.Composable public static androidx.compose.foundation.text.input.TextFieldState rememberTextFieldState(optional String initialText, optional long initialSelection);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void setTextAndPlaceCursorAtEnd(androidx.compose.foundation.text.input.TextFieldState, String text);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void setTextAndSelectAll(androidx.compose.foundation.text.input.TextFieldState, String text);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static kotlinx.coroutines.flow.Flow<androidx.compose.foundation.text.input.TextFieldCharSequence> textAsFlow(androidx.compose.foundation.text.input.TextFieldState);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static kotlinx.coroutines.flow.Flow<androidx.compose.foundation.text.input.TextFieldCharSequence> valueAsFlow(androidx.compose.foundation.text.input.TextFieldState);
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @kotlin.jvm.JvmInline public final value class TextObfuscationMode {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 652aacc..86fc321 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -1752,10 +1752,14 @@
method @kotlin.PublishedApi internal void commitEdit(androidx.compose.foundation.text.input.TextFieldBuffer newValue);
method public inline void edit(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.text.input.TextFieldBuffer,kotlin.Unit> block);
method @kotlin.PublishedApi internal void finishEditing();
- method public androidx.compose.foundation.text.input.TextFieldCharSequence getText();
+ method public androidx.compose.ui.text.TextRange? getComposition();
+ method public long getSelection();
+ method public CharSequence getText();
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.text.input.UndoState getUndoState();
- method @kotlin.PublishedApi internal androidx.compose.foundation.text.input.TextFieldBuffer startEdit(androidx.compose.foundation.text.input.TextFieldCharSequence value);
- property public final androidx.compose.foundation.text.input.TextFieldCharSequence text;
+ method @kotlin.PublishedApi internal androidx.compose.foundation.text.input.TextFieldBuffer startEdit();
+ property public final androidx.compose.ui.text.TextRange? composition;
+ property public final long selection;
+ property public final CharSequence text;
property @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final androidx.compose.foundation.text.input.UndoState undoState;
}
@@ -1767,11 +1771,10 @@
public final class TextFieldStateKt {
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void clearText(androidx.compose.foundation.text.input.TextFieldState);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend Object? forEachTextValue(androidx.compose.foundation.text.input.TextFieldState, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.text.input.TextFieldCharSequence,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<?>);
method @androidx.compose.runtime.Composable public static androidx.compose.foundation.text.input.TextFieldState rememberTextFieldState(optional String initialText, optional long initialSelection);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void setTextAndPlaceCursorAtEnd(androidx.compose.foundation.text.input.TextFieldState, String text);
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static void setTextAndSelectAll(androidx.compose.foundation.text.input.TextFieldState, String text);
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static kotlinx.coroutines.flow.Flow<androidx.compose.foundation.text.input.TextFieldCharSequence> textAsFlow(androidx.compose.foundation.text.input.TextFieldState);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static kotlinx.coroutines.flow.Flow<androidx.compose.foundation.text.input.TextFieldCharSequence> valueAsFlow(androidx.compose.foundation.text.input.TextFieldState);
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @kotlin.jvm.JvmInline public final value class TextObfuscationMode {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldOutputTransformationDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldOutputTransformationDemos.kt
index 2558c13..b3bcb53 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldOutputTransformationDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldOutputTransformationDemos.kt
@@ -79,7 +79,7 @@
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun InsertReplaceDeleteDemo() {
- val text = remember { TextFieldState("abc def ghi") }
+ val state = remember { TextFieldState("abc def ghi") }
var prefixEnabled by remember { mutableStateOf(true) }
var suffixEnabled by remember { mutableStateOf(true) }
var middleWedge by remember { mutableStateOf(true) }
@@ -136,7 +136,7 @@
}
var isFirstFieldFocused by remember { mutableStateOf(false) }
BasicTextField(
- state = text,
+ state = state,
onTextLayout = { textLayoutResultProvider = it },
modifier = Modifier
.alignByBaseline()
@@ -149,7 +149,7 @@
// Only draw selection outline when not focused.
if (isFirstFieldFocused) return@drawWithContent
val textLayoutResult = textLayoutResultProvider() ?: return@drawWithContent
- val selection = text.text.selection
+ val selection = state.selection
if (selection.collapsed) {
val cursorRect = textLayoutResult.getCursorRect(selection.start)
drawLine(
@@ -175,7 +175,7 @@
modifier = Modifier.alignBy { (it.measuredHeight * 0.75f).toInt() }
)
BasicTextField(
- state = text,
+ state = state,
modifier = Modifier
.alignByBaseline()
.weight(0.5f)
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldSamples.kt
index baed998..2604fea 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldSamples.kt
@@ -44,12 +44,11 @@
import androidx.compose.foundation.text.input.delete
import androidx.compose.foundation.text.input.forEachChange
import androidx.compose.foundation.text.input.forEachChangeReversed
-import androidx.compose.foundation.text.input.forEachTextValue
import androidx.compose.foundation.text.input.insert
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
-import androidx.compose.foundation.text.input.textAsFlow
import androidx.compose.foundation.text.input.then
+import androidx.compose.foundation.text.input.valueAsFlow
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
@@ -224,7 +223,7 @@
/** Called while the view model is active, e.g. from a LaunchedEffect. */
suspend fun run() {
- searchFieldState.forEachTextValue { queryText ->
+ searchFieldState.valueAsFlow().collectLatest { queryText ->
// Start a new search every time the user types something valid. If the previous
// search is still being processed when the text is changed, it will be cancelled
// and this code will run again with the latest query text.
@@ -426,41 +425,6 @@
})
}
-@Sampled
-fun BasicTextFieldForEachTextValueSample() {
- class SearchViewModel {
- val searchFieldState = TextFieldState()
- var searchResults: List<String> by mutableStateOf(emptyList())
- private set
-
- /** Called while the view model is active, e.g. from a LaunchedEffect. */
- suspend fun run() {
- searchFieldState.forEachTextValue { queryText ->
- // Start a new search every time the user types something. If the previous search
- // is still being processed when the text is changed, it will be cancelled and this
- // code will run again with the latest query text.
- searchResults = performSearch(query = queryText)
- }
- }
-
- private suspend fun performSearch(query: CharSequence): List<String> {
- TODO()
- }
- }
-
- @Composable
- fun SearchScreen(viewModel: SearchViewModel) {
- Column {
- BasicTextField(viewModel.searchFieldState)
- LazyColumn {
- items(viewModel.searchResults) {
- TODO()
- }
- }
- }
- }
-}
-
@OptIn(FlowPreview::class)
@Suppress("RedundantSuspendModifier")
@Sampled
@@ -472,7 +436,7 @@
/** Called while the view model is active, e.g. from a LaunchedEffect. */
suspend fun run() {
- searchFieldState.textAsFlow()
+ searchFieldState.valueAsFlow()
// Let fast typers get multiple keystrokes in before kicking off a search.
.debounce(500)
// collectLatest cancels the previous search if it's still running when there's a
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldValueSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldValueSample.kt
index 14a7720..356546a 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldValueSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BasicTextFieldValueSample.kt
@@ -19,7 +19,6 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.text.BasicTextField
-import androidx.compose.foundation.text.input.TextFieldCharSequence
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -46,7 +45,6 @@
)
}
-@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun StringTextField(
value: String,
@@ -198,20 +196,19 @@
}
private fun observeTextState(fireOnValueChanged: Boolean = true) {
- lateinit var text: TextFieldCharSequence
+ lateinit var value: TextFieldValue
observeReads {
- text = state.text
+ value = TextFieldValue(
+ state.text.toString(),
+ state.selection,
+ state.composition
+ )
}
// This code is outside of the observeReads lambda so we don't observe any state reads the
// callback happens to do.
if (fireOnValueChanged) {
- val newValue = TextFieldValue(
- text = text.toString(),
- selection = text.selection,
- composition = text.composition
- )
- onValueChanged(newValue)
+ onValueChanged(value)
}
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSemanticsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSemanticsTest.kt
index 3ad3c19..4a2d653 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSemanticsTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldSemanticsTest.kt
@@ -385,7 +385,7 @@
rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(2, 3))
+ assertThat(state.selection).isEqualTo(TextRange(2, 3))
}
}
@@ -406,7 +406,7 @@
rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(2))
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
}
}
@@ -517,7 +517,7 @@
rule.onNodeWithTag(Tag).performSemanticsAction(SemanticsActions.PasteText)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
assertThat(state.text.toString()).isEqualTo("Hello World!")
}
}
@@ -548,7 +548,7 @@
rule.onNodeWithTag(Tag).performSemanticsAction(SemanticsActions.PasteText)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(9))
+ assertThat(state.selection).isEqualTo(TextRange(9))
assertThat(state.text.toString()).isEqualTo("Heo Word!")
}
}
@@ -611,7 +611,7 @@
rule.onNodeWithTag(Tag).performSemanticsAction(SemanticsActions.CopyText)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
}
}
@@ -633,7 +633,7 @@
rule.runOnIdle {
assertThat(state.text.toString()).isEqualTo(" World!")
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
assertThat(clipboardManager.getText()?.toString()).isEqualTo("Hello")
}
}
@@ -659,7 +659,7 @@
rule.runOnIdle {
assertThat(state.text.toString()).isEqualTo("Hello World!")
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
assertThat(clipboardManager.getText()?.toString()).isEqualTo("Hello")
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
index f5952d3..6e216ca 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
@@ -1134,7 +1134,7 @@
rule.waitForIdle()
assertThat(tfs.text.toString()).isEqualTo(longText)
- assertThat(tfs.text.selection).isEqualTo(TextRange(longText.length))
+ assertThat(tfs.selection).isEqualTo(TextRange(longText.length))
}
@Test
@@ -1155,7 +1155,7 @@
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
assertThat(imm.expectCall("updateSelection(0, 5, -1, -1)"))
}
}
@@ -1230,7 +1230,7 @@
rule.runOnIdle {
assertThat(state.text.toString()).isEqualTo("Worldo")
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/DecorationBoxTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/DecorationBoxTest.kt
index 4944195..96cb15d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/DecorationBoxTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/DecorationBoxTest.kt
@@ -273,7 +273,7 @@
// assertThat selection happened
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
}
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/RememberTextFieldStateTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/RememberTextFieldStateTest.kt
index bcadfb2..2fe532a 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/RememberTextFieldStateTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/RememberTextFieldStateTest.kt
@@ -50,7 +50,7 @@
rule.runOnIdle {
assertThat(state.text.toString()).isEqualTo("hello")
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
}
@@ -78,7 +78,7 @@
rule.runOnIdle {
assertThat(restoredState.text.toString()).isEqualTo("hello, world")
- assertThat(restoredState.text.selection).isEqualTo(TextRange(0, 12))
+ assertThat(restoredState.selection).isEqualTo(TextRange(0, 12))
}
}
@@ -109,7 +109,7 @@
rule.runOnIdle {
assertThat(restoredState.text.toString()).isEqualTo("hello, world")
- assertThat(restoredState.text.selection).isEqualTo(TextRange(0, 12))
+ assertThat(restoredState.selection).isEqualTo(TextRange(0, 12))
}
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCodepointTransformationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCodepointTransformationTest.kt
index 36aa45b..bc7f1d1 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCodepointTransformationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCodepointTransformationTest.kt
@@ -437,7 +437,7 @@
listOf(0, 1, 3, 4).forEachIndexed { i, expectedCursor ->
rule.runOnIdle {
assertWithMessage("After pressing right arrow $i times")
- .that(state.text.selection).isEqualTo(TextRange(expectedCursor))
+ .that(state.selection).isEqualTo(TextRange(expectedCursor))
}
rule.onNodeWithTag(Tag).performKeyInput {
pressKey(Key.DirectionRight)
@@ -477,7 +477,7 @@
).forEachIndexed { i, expectedSelection ->
rule.runOnIdle {
assertWithMessage("After pressing shift+right arrow $i times")
- .that(state.text.selection).isEqualTo(expectedSelection)
+ .that(state.selection).isEqualTo(expectedSelection)
}
rule.onNodeWithTag(Tag).performKeyInput {
withKeyDown(Key.ShiftLeft) {
@@ -519,7 +519,7 @@
).forEachIndexed { i, expectedSelection ->
rule.runOnIdle {
assertWithMessage("After pressing shift+left arrow $i times")
- .that(state.text.selection).isEqualTo(expectedSelection)
+ .that(state.selection).isEqualTo(expectedSelection)
}
rule.onNodeWithTag(Tag).performKeyInput {
withKeyDown(Key.ShiftLeft) {
@@ -815,7 +815,7 @@
.that(performSelectionOnVisualText(write)).isTrue()
rule.runOnIdle {
assertWithMessage("Visual selection $write to mapped")
- .that(text.selection).isEqualTo(expected)
+ .that(selection).isEqualTo(expected)
}
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCursorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCursorTest.kt
index e062e6e..a982889 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCursorTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldCursorTest.kt
@@ -125,7 +125,7 @@
private var textLayoutResult: (() -> TextLayoutResult?)? = null
private val cursorRect: Rect
// assume selection is collapsed
- get() = textLayoutResult?.invoke()?.getCursorRect(state.text.selection.start)
+ get() = textLayoutResult?.invoke()?.getCursorRect(state.selection.start)
?: Rect.Zero
private val cursorSize: DpSize by lazy {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt
index 90f6621..ff0558e 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt
@@ -78,9 +78,9 @@
@Test
fun nonTextContent_isNotAccepted() {
rule.setContentAndTestDragAndDrop {
- val startSelection = state.text.selection
+ val startSelection = state.selection
drag(Offset(fontSize.toPx() * 2, 10f), defaultUri)
- assertThat(state.text.selection).isEqualTo(startSelection)
+ assertThat(state.selection).isEqualTo(startSelection)
}
}
@@ -93,7 +93,7 @@
) {
val accepted = drag(Offset(fontSize.toPx() * 2, 10f), defaultUri)
assertThat(accepted).isTrue()
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
}
@@ -101,7 +101,7 @@
fun textContent_isAccepted() {
rule.setContentAndTestDragAndDrop {
drag(Offset(fontSize.toPx() * 2, 10f), "hello")
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
}
@@ -109,11 +109,11 @@
fun draggingText_updatesSelection() {
rule.setContentAndTestDragAndDrop {
drag(Offset(fontSize.toPx() * 1, 10f), "hello")
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
drag(Offset(fontSize.toPx() * 2, 10f), "hello")
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
drag(Offset(fontSize.toPx() * 3, 10f), "hello")
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
}
}
@@ -125,11 +125,11 @@
}
) {
drag(Offset(fontSize.toPx() * 1, 10f), defaultUri)
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
drag(Offset(fontSize.toPx() * 2, 10f), defaultUri)
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
drag(Offset(fontSize.toPx() * 3, 10f), defaultUri)
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
}
}
@@ -140,9 +140,9 @@
modifier = Modifier.width(300.dp)
) {
drag(Offset.Zero, "hello")
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
drag(Offset(295.dp.toPx(), 10f), "hello")
- assertThat(state.text.selection).isEqualTo(TextRange(4))
+ assertThat(state.selection).isEqualTo(TextRange(4))
}
}
@@ -354,7 +354,7 @@
" Awesome"
)
drop()
- assertThat(state.text.selection).isEqualTo(TextRange("Hello Awesome".length))
+ assertThat(state.selection).isEqualTo(TextRange("Hello Awesome".length))
assertThat(state.text.toString()).isEqualTo("Hello Awesome World!")
}
}
@@ -378,7 +378,7 @@
}
drag(Offset(fontSize.toPx() * 5, 10f), clipData)
drop()
- assertThat(state.text.selection).isEqualTo(TextRange("Hello Awesome".length))
+ assertThat(state.selection).isEqualTo(TextRange("Hello Awesome".length))
assertThat(state.text.toString()).isEqualTo("Hello Awesome World!")
assertThat(receivedContent.clipEntry.clipData.itemCount).isEqualTo(2)
assertThat(receivedContent.clipEntry.firstUriOrNull()).isEqualTo(defaultUri)
@@ -402,7 +402,7 @@
}
drag(Offset(fontSize.toPx() * 5, 10f), clipData)
drop()
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
assertThat(state.text.toString()).isEqualTo("Hello World!")
assertThat(receivedContent.clipEntry.clipData.itemCount).isEqualTo(2)
assertThat(receivedContent.clipEntry.firstUriOrNull()).isEqualTo(defaultUri)
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
index bafdbfa..2e16280 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldKeyEventTest.kt
@@ -689,7 +689,7 @@
fun expectedSelection(selection: TextRange) {
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(selection)
+ assertThat(state.selection).isEqualTo(selection)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationGesturesIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationGesturesIntegrationTest.kt
index 599b9bf..7787cf8 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationGesturesIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationGesturesIntegrationTest.kt
@@ -77,7 +77,7 @@
click(center + Offset(1f, 0f))
}
rule.runOnIdle {
- assertThat(text.text.selection).isEqualTo(TextRange(2))
+ assertThat(text.selection).isEqualTo(TextRange(2))
}
rule.onNodeWithTag(Tag).performTouchInput {
@@ -87,7 +87,7 @@
click(center + Offset(-1f, 0f))
}
rule.runOnIdle {
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
}
}
@@ -124,7 +124,7 @@
click(topRight)
}
rule.runOnIdle {
- assertThat(text.text.selection).isEqualTo(TextRange(indexOfA))
+ assertThat(text.selection).isEqualTo(TextRange(indexOfA))
}
assertCursor(indexOfA)
@@ -134,7 +134,7 @@
click(bottomLeft)
}
rule.runOnIdle {
- assertThat(text.text.selection)
+ assertThat(text.selection)
.isEqualTo(TextRange(indexOfA + 1))
}
assertCursor(indexOfA + replacement.length)
@@ -164,7 +164,7 @@
click(center + Offset(1f, 0f))
}
rule.runOnIdle {
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
}
assertCursor(5)
@@ -175,7 +175,7 @@
click(center + Offset(-1f, 0f))
}
rule.runOnIdle {
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
}
assertCursor(1)
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationHardwareKeysIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationHardwareKeysIntegrationTest.kt
index 768fd70..c10b343 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationHardwareKeysIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldOutputTransformationHardwareKeysIntegrationTest.kt
@@ -78,13 +78,13 @@
}
rule.onNodeWithTag(Tag).requestFocus()
- assertThat(text.text.selection).isEqualTo(TextRange(0))
+ assertThat(text.selection).isEqualTo(TextRange(0))
pressKey(Key.DirectionRight)
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
pressKey(Key.DirectionRight)
- assertThat(text.text.selection).isEqualTo(TextRange(3))
+ assertThat(text.selection).isEqualTo(TextRange(3))
pressKey(Key.DirectionRight)
- assertThat(text.text.selection).isEqualTo(TextRange(4))
+ assertThat(text.selection).isEqualTo(TextRange(4))
}
@Test
@@ -100,13 +100,13 @@
}
rule.onNodeWithTag(Tag).requestFocus()
- assertThat(text.text.selection).isEqualTo(TextRange(4))
+ assertThat(text.selection).isEqualTo(TextRange(4))
pressKey(Key.DirectionLeft)
- assertThat(text.text.selection).isEqualTo(TextRange(3))
+ assertThat(text.selection).isEqualTo(TextRange(3))
pressKey(Key.DirectionLeft)
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
pressKey(Key.DirectionLeft)
- assertThat(text.text.selection).isEqualTo(TextRange(0))
+ assertThat(text.selection).isEqualTo(TextRange(0))
}
@Test
@@ -181,13 +181,13 @@
}
rule.onNodeWithTag(Tag).requestFocus()
- assertThat(text.text.selection).isEqualTo(TextRange(0))
+ assertThat(text.selection).isEqualTo(TextRange(0))
pressKey(Key.DirectionRight)
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
pressKey(Key.DirectionRight)
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
pressKey(Key.DirectionRight)
- assertThat(text.text.selection).isEqualTo(TextRange(2))
+ assertThat(text.selection).isEqualTo(TextRange(2))
}
@Test
@@ -203,13 +203,13 @@
}
rule.onNodeWithTag(Tag).requestFocus()
- assertThat(text.text.selection).isEqualTo(TextRange(2))
+ assertThat(text.selection).isEqualTo(TextRange(2))
pressKey(Key.DirectionLeft)
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
pressKey(Key.DirectionLeft)
- assertThat(text.text.selection).isEqualTo(TextRange(1))
+ assertThat(text.selection).isEqualTo(TextRange(1))
pressKey(Key.DirectionLeft)
- assertThat(text.text.selection).isEqualTo(TextRange(0))
+ assertThat(text.selection).isEqualTo(TextRange(0))
}
@Test
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldScrollTest.kt
index 8781298..6a566ee 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldScrollTest.kt
@@ -458,7 +458,7 @@
rule.runOnIdle {
assertThat(scrollState.value).isEqualTo(scrollState.maxValue)
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
}
}
@@ -591,9 +591,9 @@
rule.onNodeWithTag("field").assertTextEquals("aaaaaaaaaa")
rule.waitUntil(
"scrollState.value (${scrollState.value}) == 0 && " +
- "state.text.selection (${state.text.selection}) == TextRange(0)"
+ "state.selection (${state.selection}) == TextRange(0)"
) {
- scrollState.value == 0 && state.text.selection == TextRange(0)
+ scrollState.value == 0 && state.selection == TextRange(0)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt
index 73e32c2..f944f79 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/TextInputServiceAndroidCursorAnchorInfoTest.kt
@@ -125,8 +125,8 @@
// Immediate update.
val expectedInfo = builder.build(
text = textFieldState.text,
- selection = textFieldState.text.selection,
- composition = textFieldState.text.composition,
+ selection = textFieldState.selection,
+ composition = textFieldState.composition,
textLayoutResult = layoutState.layoutResult!!,
matrix = getAndroidMatrix(windowOffset),
innerTextFieldBounds = Rect.Zero,
@@ -186,8 +186,8 @@
// Monitoring update.
val expectedInfo = builder.build(
text = textFieldState.text,
- selection = textFieldState.text.selection,
- composition = textFieldState.text.composition,
+ selection = textFieldState.selection,
+ composition = textFieldState.composition,
textLayoutResult = layoutState.layoutResult!!,
matrix = getAndroidMatrix(Offset(67f, 89f)),
innerTextFieldBounds = Rect.Zero,
@@ -208,8 +208,8 @@
// Immediate update.
val expectedInfo = builder.build(
text = textFieldState.text,
- selection = textFieldState.text.selection,
- composition = textFieldState.text.composition,
+ selection = textFieldState.selection,
+ composition = textFieldState.composition,
textLayoutResult = layoutState.layoutResult!!,
matrix = getAndroidMatrix(windowOffset),
innerTextFieldBounds = Rect.Zero,
@@ -225,8 +225,8 @@
// Monitoring update.
val expectedInfo2 = builder.build(
text = textFieldState.text,
- selection = textFieldState.text.selection,
- composition = textFieldState.text.composition,
+ selection = textFieldState.selection,
+ composition = textFieldState.composition,
textLayoutResult = layoutState.layoutResult!!,
matrix = getAndroidMatrix(Offset(67f, 89f)),
innerTextFieldBounds = Rect.Zero,
@@ -247,8 +247,8 @@
// Immediate update.
val expectedInfo = builder.build(
text = textFieldState.text,
- selection = textFieldState.text.selection,
- composition = textFieldState.text.composition,
+ selection = textFieldState.selection,
+ composition = textFieldState.composition,
textLayoutResult = layoutState.layoutResult!!,
matrix = getAndroidMatrix(windowOffset),
innerTextFieldBounds = Rect.Zero,
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldClickToMoveCursorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldClickToMoveCursorTest.kt
index d848cac..687eb4c 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldClickToMoveCursorTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldClickToMoveCursorTest.kt
@@ -82,15 +82,15 @@
with(rule.onNodeWithTag(TAG)) {
performTouchInput { click(center) }
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
performTouchInput { click(Offset(left + 1f, top + 1f)) } // topLeft
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
performTouchInput { click(Offset(right - 1f, top + 1f)) } // topRight
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
performTouchInput { click(Offset(left + 1f, bottom - 1f)) } // bottomLeft
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
performTouchInput { click(Offset(right - 1f, bottom - 1f)) } // bottomRight
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
}
}
@@ -111,7 +111,7 @@
with(rule.onNodeWithTag(TAG)) {
performTouchInput { click(Offset((fontSize * 2).toPx(), height / 2f)) }
}
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
@Test
@@ -133,7 +133,7 @@
with(rule.onNodeWithTag(TAG)) {
performTouchInput { click(Offset(right - (fontSize * 2).toPx(), height / 2f)) }
}
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
@Test
@@ -155,7 +155,7 @@
with(rule.onNodeWithTag(TAG)) {
performTouchInput { click(Offset(right - (fontSize * 2).toPx(), height / 2f)) }
}
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
}
@Test
@@ -175,7 +175,7 @@
with(rule.onNodeWithTag(TAG)) {
performTouchInput { click(Offset((fontSize * 2).toPx(), height / 2f)) }
}
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
}
@Test
@@ -195,7 +195,7 @@
with(rule.onNodeWithTag(TAG)) {
performTouchInput { click(Offset((fontSize * 4).toPx(), height / 2f)) }
}
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
}
@Test
@@ -217,7 +217,7 @@
with(rule.onNodeWithTag(TAG)) {
performTouchInput { click(Offset(fontSize.toPx(), height / 2f)) }
}
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
}
@Test
@@ -242,7 +242,7 @@
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(6))
+ assertThat(state.selection).isEqualTo(TextRange(6))
assertThat(scrollState.value).isGreaterThan(0)
}
}
@@ -268,7 +268,7 @@
performTouchInput { click(Offset(fontSize.toPx(), bottom - 1f)) }
}
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
assertThat(scrollState.value).isGreaterThan(0)
}
@@ -300,6 +300,6 @@
performTouchInput { click(Offset(2 * fontSize.toPx(), centerY)) }
}
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldCursorHandleTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldCursorHandleTest.kt
index 728e321..8b210f4 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldCursorHandleTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldCursorHandleTest.kt
@@ -102,7 +102,7 @@
click(Offset(fontSize.toPx() * 2, fontSize.toPx() / 2))
}
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
rule.onNode(isSelectionHandle(Handle.Cursor)).assertHandlePositionMatches(
(2 * fontSize.value).dp + cursorWidth / 2,
@@ -155,7 +155,7 @@
click(Offset(fontSize.toPx() * 2, fontSize.toPx() / 2))
}
- assertThat(state.text.selection).isEqualTo(TextRange(4))
+ assertThat(state.selection).isEqualTo(TextRange(4))
}
@Test
@@ -177,7 +177,7 @@
click(Offset(fontSize.toPx() * 8, fontSize.toPx() / 2))
}
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
rule.onNode(isSelectionHandle(Handle.Cursor)).assertHandlePositionMatches(
5 * fontSizeDp + cursorWidth / 2,
@@ -206,7 +206,7 @@
click(Offset(fontSize.toPx() * 2, fontSize.toPx() / 2))
}
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
rule.onNode(isSelectionHandle(Handle.Cursor)).assertHandlePositionMatches(
(2 * fontSize.value).dp + cursorWidth / 2,
@@ -233,7 +233,7 @@
click(Offset(fontSize.toPx() * 8, fontSize.toPx() / 2))
}
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
rule.onNode(isSelectionHandle(Handle.Cursor)).assertHandlePositionMatches(
(5 * fontSize.value).dp + cursorWidth / 2,
@@ -604,7 +604,7 @@
swipeToRight(fontSizePx * 5)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange.Zero)
+ assertThat(state.selection).isEqualTo(TextRange.Zero)
}
// region ltr drag tests
@@ -629,7 +629,7 @@
swipeToRight(fontSizePx)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
}
@Test
@@ -653,7 +653,7 @@
swipeToLeft(fontSizePx)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
}
@@ -678,7 +678,7 @@
swipeToRight(getTextFieldWidth() * 2)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
}
@Test
@@ -702,7 +702,7 @@
swipeToLeft(getTextFieldWidth() * 2)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange.Zero)
+ assertThat(state.selection).isEqualTo(TextRange.Zero)
}
@Test
@@ -730,7 +730,7 @@
swipeToRight(getTextFieldWidth() * 3)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(state.text.length))
+ assertThat(state.selection).isEqualTo(TextRange(state.text.length))
}
@Test
@@ -757,12 +757,12 @@
swipeToRight(fontSizePx * 12, durationMillis = 1)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(12))
+ assertThat(state.selection).isEqualTo(TextRange(12))
}
swipeToRight(fontSizePx * 2, durationMillis = 1)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(14))
+ assertThat(state.selection).isEqualTo(TextRange(14))
}
}
@@ -792,7 +792,7 @@
swipeToRight(fontSizePx)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
@Test
@@ -818,7 +818,7 @@
swipeToLeft(fontSizePx)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
}
@Test
@@ -844,7 +844,7 @@
swipeToRight(getTextFieldWidth() * 2)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange.Zero)
+ assertThat(state.selection).isEqualTo(TextRange.Zero)
}
@Test
@@ -870,7 +870,7 @@
swipeToLeft(getTextFieldWidth() * 2)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(state.text.length))
+ assertThat(state.selection).isEqualTo(TextRange(state.text.length))
}
@Test
@@ -902,7 +902,7 @@
swipeToLeft(getTextFieldWidth() * 3)
rule.waitForIdle()
- assertThat(state.text.selection).isEqualTo(TextRange(state.text.length))
+ assertThat(state.selection).isEqualTo(TextRange(state.text.length))
}
@Test
@@ -936,12 +936,12 @@
swipeToLeft(fontSizePx * 12, durationMillis = 1)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(12))
+ assertThat(state.selection).isEqualTo(TextRange(12))
}
swipeToLeft(fontSizePx * 2, durationMillis = 1)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(14))
+ assertThat(state.selection).isEqualTo(TextRange(14))
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt
index 5d0f3ce..509edf6 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt
@@ -129,7 +129,7 @@
}
rule.onNode(isSelectionHandle(Handle.Cursor)).assertIsDisplayed()
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
}
@Test
@@ -183,7 +183,7 @@
rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertIsDisplayed()
rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
- assertThat(state.text.selection).isEqualTo(TextRange(4, 7))
+ assertThat(state.selection).isEqualTo(TextRange(4, 7))
}
@Test
@@ -203,8 +203,8 @@
rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertIsDisplayed()
rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
- assertThat(state.text.selection).isNotEqualTo(TextRange(7, 8))
- assertThat(state.text.selection.collapsed).isFalse()
+ assertThat(state.selection).isNotEqualTo(TextRange(7, 8))
+ assertThat(state.selection.collapsed).isFalse()
}
@Test
@@ -232,7 +232,7 @@
rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertIsDisplayed()
rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
- assertThat(state.text.selection).isEqualTo(TextRange(20, 23))
+ assertThat(state.selection).isEqualTo(TextRange(20, 23))
}
@Test
@@ -262,7 +262,7 @@
rule.onNode(isSelectionHandle(Handle.SelectionStart)).assertIsDisplayed()
rule.onNode(isSelectionHandle(Handle.SelectionEnd)).assertIsDisplayed()
- assertThat(state.text.selection).isEqualTo(TextRange(4, 7))
+ assertThat(state.selection).isEqualTo(TextRange(4, 7))
}
@Test
@@ -282,7 +282,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(4, 11))
+ assertThat(state.selection).isEqualTo(TextRange(4, 11))
}
@Test
@@ -302,7 +302,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(0, 7))
+ assertThat(state.selection).isEqualTo(TextRange(0, 7))
}
@Test
@@ -322,7 +322,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(4, 15))
+ assertThat(state.selection).isEqualTo(TextRange(4, 15))
}
@Test
@@ -342,7 +342,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(4, 15))
+ assertThat(state.selection).isEqualTo(TextRange(4, 15))
}
@Test
@@ -364,7 +364,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(4, 7))
+ assertThat(state.selection).isEqualTo(TextRange(4, 7))
}
//region RTL
@@ -386,7 +386,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(0, 7))
+ assertThat(state.selection).isEqualTo(TextRange(0, 7))
}
@Test
@@ -406,7 +406,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(4, 11))
+ assertThat(state.selection).isEqualTo(TextRange(4, 11))
}
@Test
@@ -426,7 +426,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(0, 11))
+ assertThat(state.selection).isEqualTo(TextRange(0, 11))
}
@Test
@@ -446,7 +446,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(0, 11))
+ assertThat(state.selection).isEqualTo(TextRange(0, 11))
}
@Test
@@ -470,7 +470,7 @@
up()
}
- assertThat(state.text.selection).isEqualTo(TextRange(4, 7))
+ assertThat(state.selection).isEqualTo(TextRange(4, 7))
}
@Test
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionHandlesTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionHandlesTest.kt
index 43f45e8..466b0a6 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionHandlesTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionHandlesTest.kt
@@ -335,13 +335,13 @@
rule.onNodeWithTag(TAG).performTouchInput { swipeLeft() }
assertHandlesNotExist()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
rule.onNodeWithTag(TAG).performTouchInput { swipeRight() }
assertHandlesDisplayed()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
}
@@ -368,7 +368,7 @@
}
assertHandlesNotExist()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
rule.onNodeWithTag(TAG).performTouchInput {
@@ -376,7 +376,7 @@
}
assertHandlesDisplayed()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
}
@@ -412,7 +412,7 @@
}
assertHandlesNotExist()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
rule.onNodeWithTag(containerTag).performTouchInput {
@@ -420,7 +420,7 @@
}
assertHandlesDisplayed()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
}
@@ -456,7 +456,7 @@
}
assertHandlesNotExist()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
rule.onNodeWithTag(containerTag).performTouchInput {
@@ -464,7 +464,7 @@
}
assertHandlesDisplayed()
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(1, 2))
+ assertThat(state.selection).isEqualTo(TextRange(1, 2))
}
}
@@ -485,7 +485,7 @@
swipeToLeft(Handle.SelectionStart, fontSizePx * 4)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 7))
+ assertThat(state.selection).isEqualTo(TextRange(0, 7))
}
}
@@ -506,7 +506,7 @@
swipeToRight(Handle.SelectionEnd, fontSizePx * 4)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(4, 11))
+ assertThat(state.selection).isEqualTo(TextRange(4, 11))
}
}
@@ -529,7 +529,7 @@
doubleClick(Offset(fontSizePx * 5, fontSizePx / 2)) // middle word
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(4, 7))
+ assertThat(state.selection).isEqualTo(TextRange(4, 7))
}
}
@@ -554,8 +554,8 @@
doubleClick(Offset(fontSizePx * 3.5f, fontSizePx / 2))
}
rule.runOnIdle {
- assertThat(state.text.selection).isNotEqualTo(TextRange(3, 4))
- assertThat(state.text.selection.collapsed).isFalse()
+ assertThat(state.selection).isNotEqualTo(TextRange(3, 4))
+ assertThat(state.selection.collapsed).isFalse()
}
}
@@ -585,7 +585,7 @@
swipeToLeft(Handle.SelectionStart, fontSizePx)
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 80))
+ assertThat(state.selection).isEqualTo(TextRange(0, 80))
}
}
@@ -615,7 +615,7 @@
// make sure that we also swipe to start on the first line
swipeToLeft(Handle.SelectionStart, fontSizePx * 10)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 80))
+ assertThat(state.selection).isEqualTo(TextRange(0, 80))
}
}
@@ -640,7 +640,7 @@
swipeToRight(Handle.SelectionEnd, fontSizePx)
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 80))
+ assertThat(state.selection).isEqualTo(TextRange(0, 80))
}
}
@@ -669,7 +669,7 @@
swipeToRight(Handle.SelectionEnd, layoutResult.size.width.toFloat())
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 80))
+ assertThat(state.selection).isEqualTo(TextRange(0, 80))
}
}
@@ -691,7 +691,7 @@
swipeToLeft(Handle.SelectionStart, fontSizePx * 2) // only move by 2 characters
rule.runOnIdle {
// selection extends by a word
- assertThat(state.text.selection).isEqualTo(TextRange(0, 7))
+ assertThat(state.selection).isEqualTo(TextRange(0, 7))
}
}
@@ -713,7 +713,7 @@
swipeToRight(Handle.SelectionEnd, fontSizePx * 2) // only move by 2 characters
rule.runOnIdle {
// selection extends by a word
- assertThat(state.text.selection).isEqualTo(TextRange(4, 11))
+ assertThat(state.selection).isEqualTo(TextRange(4, 11))
}
}
@@ -735,7 +735,7 @@
swipeToRight(Handle.SelectionStart, fontSizePx) // only move by a single character
rule.runOnIdle {
// selection shrinks by a character
- assertThat(state.text.selection).isEqualTo(TextRange(5, 7))
+ assertThat(state.selection).isEqualTo(TextRange(5, 7))
}
}
@@ -757,7 +757,7 @@
swipeToLeft(Handle.SelectionEnd, fontSizePx) // only move by a single character
rule.runOnIdle {
// selection shrinks by a character
- assertThat(state.text.selection).isEqualTo(TextRange(4, 6))
+ assertThat(state.selection).isEqualTo(TextRange(4, 6))
}
}
@@ -778,7 +778,7 @@
swipeToRight(Handle.SelectionStart, fontSizePx * 7)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(11, 7))
+ assertThat(state.selection).isEqualTo(TextRange(11, 7))
}
}
@@ -799,7 +799,7 @@
swipeToLeft(Handle.SelectionEnd, fontSizePx * 7)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(4, 0))
+ assertThat(state.selection).isEqualTo(TextRange(4, 0))
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionOnBackTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionOnBackTest.kt
index 25801cc..c7d8baa 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionOnBackTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionOnBackTest.kt
@@ -78,7 +78,7 @@
textNode.performKeyInput { pressKey(Key.Back) }
val expected = TextRange(3, 3)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(expected)
+ assertThat(state.selection).isEqualTo(expected)
}
}
@@ -106,7 +106,7 @@
textNode.performKeyInput { pressKey(Key.Back) }
val expected = TextRange(3, 3)
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(expected)
+ assertThat(state.selection).isEqualTo(expected)
assertThat(backPressed).isEqualTo(0)
}
}
@@ -129,7 +129,7 @@
// should have no effect
textNode.performKeyInput { keyDown(Key.Back) }
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(expected)
+ assertThat(state.selection).isEqualTo(expected)
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt
index 0106d69..2577a43 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt
@@ -193,7 +193,7 @@
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(5, 2))
+ assertThat(state.selection).isEqualTo(TextRange(5, 2))
assertThat(textToolbar.status).isEqualTo(TextToolbarStatus.Hidden)
}
@@ -206,7 +206,7 @@
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
assertThat(textToolbar.status).isEqualTo(TextToolbarStatus.Hidden)
}
}
@@ -226,7 +226,7 @@
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
assertThat(textToolbar.status).isEqualTo(TextToolbarStatus.Hidden)
}
}
@@ -243,7 +243,7 @@
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
assertThat(textToolbar.status).isEqualTo(TextToolbarStatus.Hidden)
}
}
@@ -262,7 +262,7 @@
}
rule.runOnIdle {
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
assertThat(textToolbar.status).isEqualTo(TextToolbarStatus.Shown)
}
}
@@ -494,7 +494,7 @@
selectAllOption?.invoke()
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
rule.runOnIdle {
assertThat(selectAllOption).isNull()
}
@@ -615,7 +615,7 @@
rule.runOnIdle {
assertThat(state.text.toString()).isEqualTo("Heworldllo")
- assertThat(state.text.selection).isEqualTo(TextRange(7))
+ assertThat(state.selection).isEqualTo(TextRange(7))
}
}
@@ -688,7 +688,7 @@
rule.runOnIdle {
assertThat(clipboardManager.getText()?.toString()).isEqualTo("Hello")
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
}
}
@@ -715,7 +715,7 @@
rule.runOnIdle {
assertThat(clipboardManager.getText()?.toString()).isEqualTo("ello")
assertThat(state.text.toString()).isEqualTo("H World!")
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
}
}
@@ -747,7 +747,7 @@
rule.runOnIdle {
assertThat(clipboardManager.getText()?.toString()).isEqualTo("ello")
assertThat(state.text.toString()).isEqualTo("Hello World!")
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
}
}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/gesture/TextFieldScrolledSelectionGestureTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/gesture/TextFieldScrolledSelectionGestureTest.kt
index cca04f5..7ac3f35 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/gesture/TextFieldScrolledSelectionGestureTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/gesture/TextFieldScrolledSelectionGestureTest.kt
@@ -143,7 +143,7 @@
fun assertSelectionEquals(selectionRange: Pair<Int, Int>) {
val (start, end) = selectionRange
- assertThat(textFieldState.text.selection).isEqualTo(TextRange(start, end))
+ assertThat(textFieldState.selection).isEqualTo(TextRange(start, end))
}
fun assertNoMagnifierExists() {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/undo/BasicTextFieldUndoTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/undo/BasicTextFieldUndoTest.kt
index e0051fd..901345d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/undo/BasicTextFieldUndoTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/undo/BasicTextFieldUndoTest.kt
@@ -212,7 +212,7 @@
state.undoState.undo()
rule.runOnIdle {
- assertThat(state.text.selection).isNotEqualTo(TextRange(7))
+ assertThat(state.selection).isNotEqualTo(TextRange(7))
}
state.undoState.redo()
@@ -368,7 +368,7 @@
private fun TextFieldState.assertTextAndSelection(text: String, selection: TextRange) {
rule.runOnIdle {
assertThat(this.text.toString()).isEqualTo(text)
- assertThat(this.text.selection).isEqualTo(selection)
+ assertThat(this.selection).isEqualTo(selection)
}
}
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateSaverTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateSaverTest.kt
index 6dbf160..9437e55 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateSaverTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateSaverTest.kt
@@ -37,7 +37,7 @@
assertNotNull(restoredState)
assertThat(restoredState.text.toString()).isEqualTo("hello, world")
- assertThat(restoredState.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(restoredState.selection).isEqualTo(TextRange(0, 5))
}
@Test
@@ -57,7 +57,7 @@
assertThat(restoredState.undoState.canUndo).isTrue()
restoredState.undoState.undo()
assertThat(restoredState.text.toString()).isEqualTo("hello, world")
- assertThat(restoredState.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(restoredState.selection).isEqualTo(TextRange(0, 5))
}
private object TestSaverScope : SaverScope {
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
index 5aab181..4afc7a0 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.text.input
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.snapshots.SnapshotStateObserver
import androidx.compose.ui.text.TextRange
@@ -27,6 +28,7 @@
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
@@ -47,21 +49,21 @@
fun defaultInitialTextAndSelection() {
val state = TextFieldState()
assertThat(state.text.toString()).isEqualTo("")
- assertThat(state.text.selection).isEqualTo(TextRange.Zero)
+ assertThat(state.selection).isEqualTo(TextRange.Zero)
}
@Test
fun customInitialTextAndDefaultSelection() {
val state = TextFieldState(initialText = "hello")
assertThat(state.text.toString()).isEqualTo("hello")
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
}
@Test
fun customInitialTextAndSelection() {
val state = TextFieldState(initialText = "hello", initialSelection = TextRange(0, 1))
assertThat(state.text.toString()).isEqualTo("hello")
- assertThat(state.text.selection).isEqualTo(TextRange(0, 1))
+ assertThat(state.selection).isEqualTo(TextRange(0, 1))
}
@Test
@@ -290,7 +292,7 @@
replace(0, 0, "hello")
placeCursorAtEnd()
}
- assertThat(state.text.selection).isEqualTo(TextRange(5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
}
@Test
@@ -299,7 +301,7 @@
replace(0, 0, "hello")
placeCursorBeforeCharAt(2)
}
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
@Test
@@ -321,7 +323,7 @@
replace(0, 0, "hello")
selectAll()
}
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(0, 5))
}
@Test
@@ -330,7 +332,7 @@
replace(0, 0, "hello")
selection = TextRange(1, 4)
}
- assertThat(state.text.selection).isEqualTo(TextRange(1, 4))
+ assertThat(state.selection).isEqualTo(TextRange(1, 4))
}
@Test
@@ -398,14 +400,7 @@
fun setTextAndPlaceCursorAtEnd_works() {
state.setTextAndPlaceCursorAtEnd("Hello")
assertThat(state.text.toString()).isEqualTo("Hello")
- assertThat(state.text.selection).isEqualTo(TextRange(5))
- }
-
- @Test
- fun setTextAndSelectAll_works() {
- state.setTextAndSelectAll("Hello")
- assertThat(state.text.toString()).isEqualTo("Hello")
- assertThat(state.text.selection).isEqualTo(TextRange(0, 5))
+ assertThat(state.selection).isEqualTo(TextRange(5))
}
@Test
@@ -466,11 +461,11 @@
val texts = mutableListOf<TextFieldCharSequence>()
launch(Dispatchers.Unconfined) {
- state.forEachTextValue { texts += it }
+ state.valueAsFlow().collectLatest { texts += it }
}
assertThat(texts).hasSize(1)
- assertThat(texts.single()).isSameInstanceAs(state.text)
+ assertThat(texts.single()).isSameInstanceAs(state.value)
assertThat(texts.single().toString()).isEqualTo("hello")
assertThat(texts.single().selection).isEqualTo(TextRange(5))
}
@@ -479,10 +474,10 @@
fun forEachValue_fires_whenTextChanged() = runTestWithSnapshotsThenCancelChildren {
val state = TextFieldState(initialSelection = TextRange(0))
val texts = mutableListOf<TextFieldCharSequence>()
- val initialText = state.text
+ val initialSelection = state.selection
launch(Dispatchers.Unconfined) {
- state.forEachTextValue { texts += it }
+ state.valueAsFlow().collectLatest { texts += it }
}
state.edit {
@@ -491,9 +486,9 @@
}
assertThat(texts).hasSize(2)
- assertThat(texts.last()).isSameInstanceAs(state.text)
+ assertThat(texts.last()).isSameInstanceAs(state.value)
assertThat(texts.last().toString()).isEqualTo("hello")
- assertThat(texts.last().selection).isEqualTo(initialText.selection)
+ assertThat(texts.last().selection).isEqualTo(initialSelection)
}
@Test
@@ -502,7 +497,7 @@
val texts = mutableListOf<TextFieldCharSequence>()
launch(Dispatchers.Unconfined) {
- state.forEachTextValue { texts += it }
+ state.valueAsFlow().collectLatest { texts += it }
}
state.edit {
@@ -510,7 +505,7 @@
}
assertThat(texts).hasSize(2)
- assertThat(texts.last()).isSameInstanceAs(state.text)
+ assertThat(texts.last()).isSameInstanceAs(state.value)
assertThat(texts.last().toString()).isEqualTo("hello")
assertThat(texts.last().selection).isEqualTo(TextRange(5))
}
@@ -521,7 +516,7 @@
val texts = mutableListOf<TextFieldCharSequence>()
launch(Dispatchers.Unconfined) {
- state.forEachTextValue { texts += it }
+ state.valueAsFlow().collectLatest { texts += it }
}
state.edit {
@@ -536,7 +531,7 @@
assertThat(texts).hasSize(3)
assertThat(texts[1].toString()).isEqualTo("hello")
- assertThat(texts[2]).isSameInstanceAs(state.text)
+ assertThat(texts[2]).isSameInstanceAs(state.value)
assertThat(texts[2].toString()).isEqualTo("hello world")
}
@@ -547,7 +542,7 @@
val texts = mutableListOf<TextFieldCharSequence>()
launch(Dispatchers.Unconfined) {
- state.forEachTextValue { texts += it }
+ state.valueAsFlow().collectLatest { texts += it }
}
state.edit {
@@ -556,7 +551,7 @@
placeCursorAtEnd()
}
- assertThat(texts.last()).isSameInstanceAs(state.text)
+ assertThat(texts.last()).isSameInstanceAs(state.value)
assertThat(texts.last().toString()).isEqualTo("hello world")
}
@@ -567,7 +562,7 @@
val texts = mutableListOf<TextFieldCharSequence>()
launch(Dispatchers.Unconfined) {
- state.forEachTextValue { texts += it }
+ state.valueAsFlow().collectLatest { texts += it }
}
val snapshot = Snapshot.takeMutableSnapshot()
@@ -583,7 +578,7 @@
snapshot.apply()
snapshot.dispose()
- assertThat(texts.last()).isSameInstanceAs(state.text)
+ assertThat(texts.last()).isSameInstanceAs(state.value)
}
@Test
@@ -593,7 +588,7 @@
val texts = mutableListOf<TextFieldCharSequence>()
launch(Dispatchers.Unconfined) {
- state.forEachTextValue { texts += it }
+ state.valueAsFlow().collectLatest { texts += it }
}
val snapshot = Snapshot.takeMutableSnapshot()
@@ -617,7 +612,7 @@
val texts = mutableListOf<TextFieldCharSequence>()
launch(Dispatchers.Unconfined) {
- state.forEachTextValue {
+ state.valueAsFlow().collectLatest {
texts += it
awaitCancellation()
}
@@ -631,6 +626,32 @@
.inOrder()
}
+ @Test
+ fun snapshotFlowOfText_onlyFiresIfContentChanges() {
+ runTestWithSnapshotsThenCancelChildren {
+ val state = TextFieldState()
+ val texts = mutableListOf<CharSequence>()
+
+ launch(Dispatchers.Unconfined) {
+ snapshotFlow {
+ state.text
+ }.collect {
+ texts += it
+ }
+ }
+
+ state.edit { append("a") }
+ state.edit { append("b") }
+ state.edit { placeCursorBeforeCharAt(0) }
+ state.edit { placeCursorAtEnd() }
+ state.edit { append("c") }
+
+ assertThat(texts.map { it.toString() })
+ .containsExactly("", "a", "ab", "abc")
+ .inOrder()
+ }
+ }
+
private fun runTestWithSnapshotsThenCancelChildren(testBody: suspend TestScope.() -> Unit) {
val globalWriteObserverHandle = Snapshot.registerGlobalWriteObserver {
// This is normally done by the compose runtime.
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldStateInternalBufferTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldStateInternalBufferTest.kt
index 168217a..bd3f689 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldStateInternalBufferTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TextFieldStateInternalBufferTest.kt
@@ -36,7 +36,7 @@
val firstValue = TextFieldCharSequence("ABCDE", TextRange.Zero)
val state = TextFieldState(firstValue)
- assertThat(state.text).isEqualTo(firstValue)
+ assertThat(state.value).isEqualTo(firstValue)
}
@Test
@@ -51,7 +51,7 @@
}
state.editAsUser { commitText("X", 1) }
- val newState = state.text
+ val newState = state.value
assertThat(newState.toString()).isEqualTo("XABCDE")
assertThat(newState.selection.min).isEqualTo(1)
@@ -73,7 +73,7 @@
}
state.editAsUser { setSelection(0, 2) }
- val newState = state.text
+ val newState = state.value
assertThat(newState.toString()).isEqualTo("ABCDE")
assertThat(newState.selection.min).isEqualTo(0)
@@ -241,13 +241,13 @@
val newValue =
TextFieldCharSequence(
"cd",
- state.text.selection,
- state.text.composition
+ state.selection,
+ state.composition
)
state.resetStateAndNotifyIme(newValue)
assertThat(state.text.toString()).isEqualTo(newValue.toString())
- assertThat(state.text.composition).isNull()
+ assertThat(state.composition).isNull()
assertThat(resetCalled).isEqualTo(1)
assertThat(selectionCalled).isEqualTo(1)
}
@@ -267,13 +267,13 @@
val newValue =
TextFieldCharSequence(
state.text,
- state.text.selection,
- state.text.composition
+ state.selection,
+ state.composition
)
state.resetStateAndNotifyIme(newValue)
assertThat(state.text.toString()).isEqualTo(newValue.toString())
- assertThat(state.text.composition).isEqualTo(composition)
+ assertThat(state.composition).isEqualTo(composition)
}
@Test
@@ -290,13 +290,13 @@
val newValue =
TextFieldCharSequence(
state.text,
- state.text.selection,
+ state.selection,
composition = TextRange(0, 2)
)
state.resetStateAndNotifyIme(newValue)
assertThat(state.text.toString()).isEqualTo(newValue.toString())
- assertThat(state.text.composition).isNull()
+ assertThat(state.composition).isNull()
}
@Test
@@ -312,13 +312,13 @@
// change the composition
val newValue = TextFieldCharSequence(
state.text,
- state.text.selection,
+ state.selection,
composition = TextRange(0, 1)
)
state.resetStateAndNotifyIme(newValue)
assertThat(state.text.toString()).isEqualTo(newValue.toString())
- assertThat(state.text.composition).isNull()
+ assertThat(state.composition).isNull()
}
@Test
@@ -340,13 +340,13 @@
val newValue = TextFieldCharSequence(
state.text,
selection = newSelection,
- composition = state.text.composition
+ composition = state.composition
)
state.resetStateAndNotifyIme(newValue)
assertThat(state.text.toString()).isEqualTo(newValue.toString())
- assertThat(state.text.composition).isEqualTo(composition)
- assertThat(state.text.selection).isEqualTo(newSelection)
+ assertThat(state.composition).isEqualTo(composition)
+ assertThat(state.selection).isEqualTo(newSelection)
}
@Test
@@ -541,7 +541,7 @@
state.editAsUser { setComposingRegion(2, 3) }
- assertThat(state.text.composition).isEqualTo(TextRange(2, 3))
+ assertThat(state.composition).isEqualTo(TextRange(2, 3))
}
@Test
@@ -552,7 +552,7 @@
state.editAsUser { setComposingRegion(2, 3) }
- assertThat(state.text.composition).isEqualTo(TextRange(2, 3))
+ assertThat(state.composition).isEqualTo(TextRange(2, 3))
}
private fun TextFieldState(
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextSelectionMovementTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextSelectionMovementTest.kt
index 029b76d..f9ce97e 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextSelectionMovementTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextSelectionMovementTest.kt
@@ -43,7 +43,7 @@
calculateNextCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(3))
}
@@ -59,7 +59,7 @@
calculatePreviousCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(1))
}
@@ -73,19 +73,19 @@
TransformedTextFieldState(state, outputTransformation = outputTransformation)
calculateNextCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(1))
assertThat(transformedState.selectionWedgeAffinity)
.isEqualTo(SelectionWedgeAffinity(WedgeAffinity.Start))
calculateNextCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(3))
assertThat(transformedState.selectionWedgeAffinity)
.isEqualTo(SelectionWedgeAffinity(WedgeAffinity.End))
calculateNextCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(4))
assertThat(transformedState.selectionWedgeAffinity)
.isEqualTo(SelectionWedgeAffinity(WedgeAffinity.End))
@@ -102,19 +102,19 @@
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(4))
calculatePreviousCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(3))
assertThat(transformedState.selectionWedgeAffinity)
.isEqualTo(SelectionWedgeAffinity(WedgeAffinity.End))
calculatePreviousCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(1))
assertThat(transformedState.selectionWedgeAffinity)
.isEqualTo(SelectionWedgeAffinity(WedgeAffinity.Start))
calculatePreviousCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(0))
assertThat(transformedState.selectionWedgeAffinity)
.isEqualTo(SelectionWedgeAffinity(WedgeAffinity.Start))
@@ -130,11 +130,11 @@
TransformedTextFieldState(state, outputTransformation = outputTransformation)
calculateNextCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(1, 3))
+ assertThat(state.selection).isEqualTo(TextRange(1, 3))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(1))
calculateNextCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(4))
+ assertThat(state.selection).isEqualTo(TextRange(4))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(2))
}
@@ -149,11 +149,11 @@
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(2))
calculatePreviousCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(1, 3))
+ assertThat(state.selection).isEqualTo(TextRange(1, 3))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(1))
calculatePreviousCursorPosition(transformedState)
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
assertThat(transformedState.visualText.selection).isEqualTo(TextRange(0))
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoTest.kt
index 6c07153..ea0d8d1 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/internal/undo/TextUndoTest.kt
@@ -76,7 +76,7 @@
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("ac")
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("")
@@ -160,15 +160,15 @@
state.typeAtStart("c") // "|a" -> "c|a"
assertThat(state.text.toString()).isEqualTo("ca")
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("a")
- assertThat(state.text.selection).isEqualTo(TextRange(0))
+ assertThat(state.selection).isEqualTo(TextRange(0))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("ab")
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("")
@@ -182,15 +182,15 @@
state.typeAtEnd("g") // "defg|"
assertThat(state.text.toString()).isEqualTo("defg")
- assertThat(state.text.selection).isEqualTo(TextRange(4))
+ assertThat(state.selection).isEqualTo(TextRange(4))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("def")
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("abcd")
- assertThat(state.text.selection).isEqualTo(TextRange(4))
+ assertThat(state.selection).isEqualTo(TextRange(4))
}
@Test
@@ -202,15 +202,15 @@
state.deleteAt(2) // "de|"
assertThat(state.text.toString()).isEqualTo("de")
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("def")
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("ab")
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
}
@Test
@@ -222,15 +222,15 @@
state.typeAtEnd("c") // "ab\nc|"
assertThat(state.text.toString()).isEqualTo("ab\nc")
- assertThat(state.text.selection).isEqualTo(TextRange(4))
+ assertThat(state.selection).isEqualTo(TextRange(4))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("ab\n")
- assertThat(state.text.selection).isEqualTo(TextRange(3))
+ assertThat(state.selection).isEqualTo(TextRange(3))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("ab")
- assertThat(state.text.selection).isEqualTo(TextRange(2))
+ assertThat(state.selection).isEqualTo(TextRange(2))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("")
@@ -243,11 +243,11 @@
state.type("d") // "d|c"
assertThat(state.text.toString()).isEqualTo("dc")
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
state.undoState.undo()
assertThat(state.text.toString()).isEqualTo("abc")
- assertThat(state.text.selection).isEqualTo(TextRange(2, 0))
+ assertThat(state.selection).isEqualTo(TextRange(2, 0))
}
@Test
@@ -258,14 +258,14 @@
state.type("e") // "e|cd"
assertThat(state.text.toString()).isEqualTo("ecd")
- assertThat(state.text.selection).isEqualTo(TextRange(1))
+ assertThat(state.selection).isEqualTo(TextRange(1))
state.undoState.undo() // "|ab|cd"
state.undoState.undo() // "|abc"
state.undoState.redo() // "abcd|"
assertThat(state.text.toString()).isEqualTo("abcd")
- assertThat(state.text.selection).isEqualTo(TextRange(4, 4))
+ assertThat(state.selection).isEqualTo(TextRange(4, 4))
}
@Test
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldCharSequence.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldCharSequence.kt
index 5b32a59..14daca4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldCharSequence.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldCharSequence.kt
@@ -77,6 +77,16 @@
): TextFieldCharSequence = TextFieldCharSequenceWrapper(text, selection, composition)
/**
+ * Returns the backing CharSequence object that this TextFieldCharSequence is wrapping. This is
+ * useful for external equality comparisons that cannot use [TextFieldCharSequence.contentEquals].
+ */
+internal fun TextFieldCharSequence.getBackingCharSequence(): CharSequence {
+ return when (this) {
+ is TextFieldCharSequenceWrapper -> this.text
+ }
+}
+
+/**
* Copies the contents of this sequence from [[sourceStartIndex], [sourceEndIndex]) into
* [destination] starting at [destinationOffset].
*/
@@ -94,11 +104,22 @@
@OptIn(ExperimentalFoundationApi::class)
private class TextFieldCharSequenceWrapper(
- private val text: CharSequence,
+ text: CharSequence,
selection: TextRange,
composition: TextRange?
) : TextFieldCharSequence {
+ /**
+ * If this TextFieldCharSequence is actually a copy of another, make sure to use the backing
+ * CharSequence object to stop unnecessary nesting and logic that depends on exact equality of
+ * CharSequence comparison that's using [CharSequence.equals].
+ */
+ val text: CharSequence = if (text is TextFieldCharSequenceWrapper) {
+ text.text
+ } else {
+ text
+ }
+
override val length: Int
get() = text.length
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
index 2a587b9..2e879b1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
@@ -35,7 +35,6 @@
import androidx.compose.ui.text.coerceIn
import androidx.compose.ui.text.input.TextFieldValue
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collectLatest
internal fun TextFieldState(initialValue: TextFieldValue): TextFieldState {
return TextFieldState(
@@ -49,8 +48,11 @@
* cursor or selection.
*
* To change the text field contents programmatically, call [edit], [setTextAndSelectAll],
- * [setTextAndPlaceCursorAtEnd], or [clearText]. To observe the value of the field over time, call
- * [forEachTextValue] or [textAsFlow].
+ * [setTextAndPlaceCursorAtEnd], or [clearText]. Individual parts of the state like [text],
+ * [selection], or [composition] can be read from any snapshot restart scope like Composable
+ * functions. To observe these members from outside a restart scope, use
+ * `snapshotFlow { textFieldState.text }` or `snapshotFlow { textFieldState.selection }`. To
+ * observe the entirety of state including text, selection, and composition, call [valueAsFlow].
*
* When instantiating this class from a composable, use [rememberTextFieldState] to automatically
* save and restore the field state. For more advanced use cases, pass [TextFieldState.Saver] to
@@ -96,29 +98,67 @@
private var isEditing: Boolean by mutableStateOf(false)
/**
- * The current text and selection. This value will automatically update when the user enters
- * text or otherwise changes the text field contents. To change it programmatically, call
- * [edit].
+ * The current text, selection, and composing region. This value will automatically update when
+ * the user enters text or otherwise changes the text field contents. To change it
+ * programmatically, call [edit].
*
* This is backed by snapshot state, so reading this property in a restartable function (e.g.
* a composable function) will cause the function to restart when the text field's value
* changes.
*
- * To observe changes to this property outside a restartable function, see [forEachTextValue]
- * and [textAsFlow].
+ * To observe changes to this property outside a restartable function, see [valueAsFlow].
*
* @sample androidx.compose.foundation.samples.BasicTextFieldTextDerivedStateSample
*
* @see edit
- * @see forEachTextValue
- * @see textAsFlow
+ * @see valueAsFlow
*/
- var text: TextFieldCharSequence by mutableStateOf(
+ internal var value: TextFieldCharSequence by mutableStateOf(
TextFieldCharSequence(initialText, initialSelection)
)
private set
/**
+ * The current text content. This value will automatically update when the user enters text or
+ * otherwise changes the text field contents. To change it programmatically, call [edit].
+ *
+ * To observe changes to this property outside a restartable function, use
+ * `snapshotFlow { text }`.
+ *
+ * @sample androidx.compose.foundation.samples.BasicTextFieldTextValuesSample
+ *
+ * @see edit
+ * @see snapshotFlow
+ */
+ val text: CharSequence get() = value.getBackingCharSequence()
+
+ /**
+ * The current selection range. If the selection is collapsed, it represents cursor location.
+ * This value will automatically update when the user enters text or otherwise changes the text
+ * field selection range. To change it programmatically, call [edit].
+ *
+ * To observe changes to this property outside a restartable function, use
+ * `snapshotFlow { selection }`.
+ *
+ * @see edit
+ * @see snapshotFlow
+ * @see TextFieldCharSequence.selection
+ */
+ val selection: TextRange get() = value.selection
+
+ /**
+ * The current composing range dictated by the IME. If null, there is no composing region.
+ *
+ * To observe changes to this property outside a restartable function, use
+ * `snapshotFlow { composition }`.
+ *
+ * @see edit
+ * @see snapshotFlow
+ * @see TextFieldCharSequence.composition
+ */
+ val composition: TextRange? get() = value.composition
+
+ /**
* Runs [block] with a mutable version of the current state. The block can make changes to the
* text and cursor/selection. See the documentation on [TextFieldBuffer] for a more detailed
* description of the available operations.
@@ -133,7 +173,7 @@
* @see setTextAndSelectAll
*/
inline fun edit(block: TextFieldBuffer.() -> Unit) {
- val mutableValue = startEdit(text)
+ val mutableValue = startEdit()
try {
mutableValue.block()
commitEdit(mutableValue)
@@ -143,7 +183,7 @@
}
override fun toString(): String =
- "TextFieldState(selection=${text.selection}, text=\"$text\")"
+ "TextFieldState(selection=$selection, text=\"$text\")"
/**
* Undo history controller for this TextFieldState.
@@ -159,7 +199,7 @@
@Suppress("ShowingMemberInHiddenClass")
@PublishedApi
- internal fun startEdit(value: TextFieldCharSequence): TextFieldBuffer {
+ internal fun startEdit(): TextFieldBuffer {
check(!isEditing) {
"TextFieldState does not support concurrent or nested editing."
}
@@ -218,7 +258,7 @@
undoBehavior: TextFieldEditUndoBehavior = TextFieldEditUndoBehavior.MergeIfPossible,
block: EditingBuffer.() -> Unit
) {
- val previousValue = text
+ val previousValue = value
mainBuffer.changeTracker.clearChanges()
mainBuffer.block()
@@ -249,7 +289,7 @@
* a public API.
*/
internal inline fun editWithNoSideEffects(block: EditingBuffer.() -> Unit) {
- val previousValue = text
+ val previousValue = value
mainBuffer.changeTracker.clearChanges()
mainBuffer.block()
@@ -260,7 +300,7 @@
composition = mainBuffer.composition
)
- text = afterEditValue
+ value = afterEditValue
sendChangesToIme(
oldValue = previousValue,
newValue = afterEditValue,
@@ -281,24 +321,24 @@
)
if (inputTransformation == null) {
- val oldValue = text
- text = afterEditValue
+ val oldValue = value
+ value = afterEditValue
sendChangesToIme(
oldValue = oldValue,
newValue = afterEditValue,
restartImeIfContentChanges = restartImeIfContentChanges
)
- recordEditForUndo(previousValue, text, mainBuffer.changeTracker, undoBehavior)
+ recordEditForUndo(previousValue, value, mainBuffer.changeTracker, undoBehavior)
return
}
- val oldValue = text
+ val oldValue = value
// if only difference is composition, don't run filter, don't send it to undo manager
if (afterEditValue.contentEquals(oldValue) &&
afterEditValue.selection == oldValue.selection
) {
- text = afterEditValue
+ value = afterEditValue
sendChangesToIme(
oldValue = oldValue,
newValue = afterEditValue,
@@ -307,22 +347,22 @@
return
}
- val mutableValue = TextFieldBuffer(
+ val textFieldBuffer = TextFieldBuffer(
initialValue = afterEditValue,
sourceValue = oldValue,
initialChanges = mainBuffer.changeTracker
)
inputTransformation.transformInput(
originalValue = oldValue,
- valueWithChanges = mutableValue
+ valueWithChanges = textFieldBuffer
)
// If neither the text nor the selection changed, we want to preserve the composition.
// Otherwise, the IME will reset it anyway.
- val afterFilterValue = mutableValue.toTextFieldCharSequence(
+ val afterFilterValue = textFieldBuffer.toTextFieldCharSequence(
composition = afterEditValue.composition
)
if (afterFilterValue == afterEditValue) {
- text = afterFilterValue
+ value = afterFilterValue
sendChangesToIme(
oldValue = oldValue,
newValue = afterEditValue,
@@ -332,7 +372,7 @@
resetStateAndNotifyIme(afterFilterValue)
}
// mutableValue contains all the changes from user and the filter.
- recordEditForUndo(previousValue, text, mutableValue.changes, undoBehavior)
+ recordEditForUndo(previousValue, value, textFieldBuffer.changes, undoBehavior)
}
/**
@@ -445,15 +485,15 @@
}
val finalValue = TextFieldCharSequence(
- if (textChanged) newValue else bufferState,
- mainBuffer.selection,
- mainBuffer.composition
+ text = if (textChanged) newValue else bufferState,
+ selection = mainBuffer.selection,
+ composition = mainBuffer.composition
)
// value must be set before notifyImeListeners are called. Even though we are sending the
// previous and current values, a system callback may request the latest state e.g. IME
// restartInput call is handled before notifyImeListeners return.
- text = finalValue
+ value = finalValue
sendChangesToIme(
oldValue = bufferState,
@@ -489,8 +529,8 @@
override fun SaverScope.save(value: TextFieldState): Any? {
return listOf(
value.text.toString(),
- value.text.selection.start,
- value.text.selection.end,
+ value.selection.start,
+ value.selection.end,
with(TextUndoManager.Companion.Saver) {
save(value.textUndoManager)
}
@@ -514,13 +554,14 @@
}
/**
- * Returns a [Flow] of the values of [TextFieldState.text] as seen from the global snapshot.
+ * Returns a [Flow] of the values of [TextFieldState.text], [TextFieldState.selection], and
+ * [TextFieldState.composition] as seen from the global snapshot.
* The initial value is emitted immediately when the flow is collected.
*
* @sample androidx.compose.foundation.samples.BasicTextFieldTextValuesSample
*/
@ExperimentalFoundationApi
-fun TextFieldState.textAsFlow(): Flow<TextFieldCharSequence> = snapshotFlow { text }
+fun TextFieldState.valueAsFlow(): Flow<TextFieldCharSequence> = snapshotFlow { value }
/**
* Create and remember a [TextFieldState]. The state is remembered using [rememberSaveable] and so
@@ -609,28 +650,3 @@
placeCursorAtEnd()
}
}
-
-/**
- * Invokes [block] with the value of [TextFieldState.text], and every time the value is changed.
- *
- * The caller will be suspended until its coroutine is cancelled. If the text is changed while
- * [block] is suspended, [block] will be cancelled and re-executed with the new value immediately.
- * [block] will never be executed concurrently with itself.
- *
- * To get access to a [Flow] of [TextFieldState.text] over time, use [textAsFlow].
- *
- * Warning: Do not update the value of the [TextFieldState] from [block]. If you want to perform
- * either a side effect when text is changed, or filter it in some way, use an
- * [InputTransformation].
- *
- * @sample androidx.compose.foundation.samples.BasicTextFieldForEachTextValueSample
- *
- * @see textAsFlow
- */
-@ExperimentalFoundationApi
-suspend fun TextFieldState.forEachTextValue(
- block: suspend (TextFieldCharSequence) -> Unit
-): Nothing {
- textAsFlow().collectLatest(block)
- error("textAsFlow expected not to complete without exception")
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt
index c5b1415..990e981 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TransformedTextFieldState.kt
@@ -116,7 +116,7 @@
derivedStateOf {
// text is a state read. transformation may also perform state reads when ran.
calculateTransformedText(
- untransformedText = textFieldState.text,
+ untransformedValue = textFieldState.value,
outputTransformation = transformation,
wedgeAffinity = selectionWedgeAffinity
)
@@ -129,7 +129,7 @@
calculateTransformedText(
// These are state reads. codepointTransformation may also perform state reads
// when ran.
- untransformedText = outputTransformedText?.value?.text ?: textFieldState.text,
+ untransformedValue = outputTransformedText?.value?.text ?: textFieldState.value,
codepointTransformation = transformation,
wedgeAffinity = selectionWedgeAffinity
)
@@ -141,7 +141,7 @@
* [CodepointTransformation] applied.
*/
val untransformedText: TextFieldCharSequence
- get() = textFieldState.text
+ get() = textFieldState.value
/**
* The text that should be presented to the user in most cases. If an [OutputTransformation] is
@@ -433,22 +433,22 @@
/**
* Applies an [OutputTransformation] to a [TextFieldCharSequence], returning the
- * transformed text content, the selection/cursor from the [untransformedText] mapped to the
+ * transformed text content, the selection/cursor from the [untransformedValue] mapped to the
* offsets in the transformed text, and an [OffsetMappingCalculator] that can be used to map
* offsets in both directions between the transformed and untransformed text.
*
- * This function is relatively expensive, since it creates a copy of [untransformedText], so
+ * This function is relatively expensive, since it creates a copy of [untransformedValue], so
* its result should be cached.
*/
@kotlin.jvm.JvmStatic
private fun calculateTransformedText(
- untransformedText: TextFieldCharSequence,
+ untransformedValue: TextFieldCharSequence,
outputTransformation: OutputTransformation,
wedgeAffinity: SelectionWedgeAffinity
): TransformedText? {
val offsetMappingCalculator = OffsetMappingCalculator()
val buffer = TextFieldBuffer(
- initialValue = untransformedText,
+ initialValue = untransformedValue,
offsetMappingCalculator = offsetMappingCalculator
)
@@ -464,11 +464,11 @@
// Pass the calculator explicitly since the one on transformedText won't be updated
// yet.
selection = mapToTransformed(
- range = untransformedText.selection,
+ range = untransformedValue.selection,
mapping = offsetMappingCalculator,
wedgeAffinity = wedgeAffinity
),
- composition = untransformedText.composition?.let {
+ composition = untransformedValue.composition?.let {
mapToTransformed(
range = it,
mapping = offsetMappingCalculator,
@@ -481,16 +481,16 @@
/**
* Applies a [CodepointTransformation] to a [TextFieldCharSequence], returning the
- * transformed text content, the selection/cursor from the [untransformedText] mapped to the
+ * transformed text content, the selection/cursor from the [untransformedValue] mapped to the
* offsets in the transformed text, and an [OffsetMappingCalculator] that can be used to map
* offsets in both directions between the transformed and untransformed text.
*
- * This function is relatively expensive, since it creates a copy of [untransformedText], so
+ * This function is relatively expensive, since it creates a copy of [untransformedValue], so
* its result should be cached.
*/
@kotlin.jvm.JvmStatic
private fun calculateTransformedText(
- untransformedText: TextFieldCharSequence,
+ untransformedValue: TextFieldCharSequence,
codepointTransformation: CodepointTransformation,
wedgeAffinity: SelectionWedgeAffinity
): TransformedText? {
@@ -498,10 +498,10 @@
// This is the call to external code. Returns same instance if no codepoints change.
val transformedText =
- untransformedText.toVisualText(codepointTransformation, offsetMappingCalculator)
+ untransformedValue.toVisualText(codepointTransformation, offsetMappingCalculator)
// Avoid allocations + mapping if there weren't actually any transformations.
- if (transformedText === untransformedText) {
+ if (transformedText === untransformedValue) {
return null
}
@@ -510,11 +510,11 @@
// Pass the calculator explicitly since the one on transformedText won't be updated
// yet.
selection = mapToTransformed(
- untransformedText.selection,
+ untransformedValue.selection,
offsetMappingCalculator,
wedgeAffinity
),
- composition = untransformedText.composition?.let {
+ composition = untransformedValue.composition?.let {
mapToTransformed(it, offsetMappingCalculator, wedgeAffinity)
}
)
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index e58e6b2f..6dc536c 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -1068,8 +1068,10 @@
}
public final class NavigationDrawerKt {
+ method @androidx.compose.runtime.Composable public static void DismissibleDrawerSheet(androidx.compose.material3.DrawerState drawerState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void DismissibleDrawerSheet(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void DismissibleNavigationDrawer(kotlin.jvm.functions.Function0<kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DrawerState drawerState, optional boolean gesturesEnabled, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ModalDrawerSheet(androidx.compose.material3.DrawerState drawerState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ModalDrawerSheet(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ModalNavigationDrawer(kotlin.jvm.functions.Function0<kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DrawerState drawerState, optional boolean gesturesEnabled, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void NavigationDrawerItem(kotlin.jvm.functions.Function0<kotlin.Unit> label, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.NavigationDrawerItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
@@ -2105,6 +2107,46 @@
}
+package androidx.compose.material3.carousel {
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class CarouselDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior multiBrowseFlingBehavior(androidx.compose.material3.carousel.CarouselState state, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior noSnapFlingBehavior();
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior singleAdvanceFlingBehavior(androidx.compose.material3.carousel.CarouselState state, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec);
+ field public static final androidx.compose.material3.carousel.CarouselDefaults INSTANCE;
+ }
+
+ public final class CarouselKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalMultiBrowseCarousel(androidx.compose.material3.carousel.CarouselState state, float preferredItemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional float minSmallItemWidth, optional float maxSmallItemWidth, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalUncontainedCarousel(androidx.compose.material3.carousel.CarouselState state, float itemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselScope {
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class CarouselState implements androidx.compose.foundation.gestures.ScrollableState {
+ ctor public CarouselState(optional int currentItem, optional @FloatRange(from=-0.5, to=0.5) float currentItemOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> itemCount);
+ method public float dispatchRawDelta(float delta);
+ method public androidx.compose.runtime.MutableState<kotlin.jvm.functions.Function0<java.lang.Integer>> getItemCountState();
+ method public boolean isScrollInProgress();
+ method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public void setItemCountState(androidx.compose.runtime.MutableState<kotlin.jvm.functions.Function0<java.lang.Integer>>);
+ property public boolean isScrollInProgress;
+ property public final androidx.compose.runtime.MutableState<kotlin.jvm.functions.Function0<java.lang.Integer>> itemCountState;
+ field public static final androidx.compose.material3.carousel.CarouselState.Companion Companion;
+ }
+
+ public static final class CarouselState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.carousel.CarouselState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.compose.material3.carousel.CarouselState,?> Saver;
+ }
+
+ public final class CarouselStateKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.carousel.CarouselState rememberCarouselState(optional int initialItem, kotlin.jvm.functions.Function0<java.lang.Integer> itemCount);
+ }
+
+}
+
package androidx.compose.material3.pulltorefresh {
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class PullToRefreshDefaults {
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index e58e6b2f..6dc536c 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -1068,8 +1068,10 @@
}
public final class NavigationDrawerKt {
+ method @androidx.compose.runtime.Composable public static void DismissibleDrawerSheet(androidx.compose.material3.DrawerState drawerState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void DismissibleDrawerSheet(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void DismissibleNavigationDrawer(kotlin.jvm.functions.Function0<kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DrawerState drawerState, optional boolean gesturesEnabled, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method @androidx.compose.runtime.Composable public static void ModalDrawerSheet(androidx.compose.material3.DrawerState drawerState, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ModalDrawerSheet(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape drawerShape, optional long drawerContainerColor, optional long drawerContentColor, optional float drawerTonalElevation, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ModalNavigationDrawer(kotlin.jvm.functions.Function0<kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.DrawerState drawerState, optional boolean gesturesEnabled, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void NavigationDrawerItem(kotlin.jvm.functions.Function0<kotlin.Unit> label, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.NavigationDrawerItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
@@ -2105,6 +2107,46 @@
}
+package androidx.compose.material3.carousel {
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class CarouselDefaults {
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior multiBrowseFlingBehavior(androidx.compose.material3.carousel.CarouselState state, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior noSnapFlingBehavior();
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.TargetedFlingBehavior singleAdvanceFlingBehavior(androidx.compose.material3.carousel.CarouselState state, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec);
+ field public static final androidx.compose.material3.carousel.CarouselDefaults INSTANCE;
+ }
+
+ public final class CarouselKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalMultiBrowseCarousel(androidx.compose.material3.carousel.CarouselState state, float preferredItemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional float minSmallItemWidth, optional float maxSmallItemWidth, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void HorizontalUncontainedCarousel(androidx.compose.material3.carousel.CarouselState state, float itemWidth, optional androidx.compose.ui.Modifier modifier, optional float itemSpacing, optional androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function2<? super androidx.compose.material3.carousel.CarouselScope,? super java.lang.Integer,kotlin.Unit> content);
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselScope {
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class CarouselState implements androidx.compose.foundation.gestures.ScrollableState {
+ ctor public CarouselState(optional int currentItem, optional @FloatRange(from=-0.5, to=0.5) float currentItemOffsetFraction, kotlin.jvm.functions.Function0<java.lang.Integer> itemCount);
+ method public float dispatchRawDelta(float delta);
+ method public androidx.compose.runtime.MutableState<kotlin.jvm.functions.Function0<java.lang.Integer>> getItemCountState();
+ method public boolean isScrollInProgress();
+ method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public void setItemCountState(androidx.compose.runtime.MutableState<kotlin.jvm.functions.Function0<java.lang.Integer>>);
+ property public boolean isScrollInProgress;
+ property public final androidx.compose.runtime.MutableState<kotlin.jvm.functions.Function0<java.lang.Integer>> itemCountState;
+ field public static final androidx.compose.material3.carousel.CarouselState.Companion Companion;
+ }
+
+ public static final class CarouselState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.carousel.CarouselState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.compose.material3.carousel.CarouselState,?> Saver;
+ }
+
+ public final class CarouselStateKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.carousel.CarouselState rememberCarouselState(optional int initialItem, kotlin.jvm.functions.Function0<java.lang.Integer> itemCount);
+ }
+
+}
+
package androidx.compose.material3.pulltorefresh {
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class PullToRefreshDefaults {
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
index 814b0841..af81870 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Components.kt
@@ -444,7 +444,7 @@
BottomSheets,
Buttons,
Card,
- // Carousel, // TODO: Re-enable when ready
+ Carousel,
Checkboxes,
Chips,
DatePickers,
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index e7b564a..037d0dbf 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -39,7 +39,6 @@
import androidx.compose.material3.samples.ButtonSample
import androidx.compose.material3.samples.ButtonWithIconSample
import androidx.compose.material3.samples.CardSample
-import androidx.compose.material3.samples.CarouselSample
import androidx.compose.material3.samples.CheckboxSample
import androidx.compose.material3.samples.CheckboxWithTextSample
import androidx.compose.material3.samples.ChipGroupReflowSample
@@ -79,6 +78,8 @@
import androidx.compose.material3.samples.FilterChipSample
import androidx.compose.material3.samples.FilterChipWithLeadingIconSample
import androidx.compose.material3.samples.FloatingActionButtonSample
+import androidx.compose.material3.samples.HorizontalMultiBrowseCarouselSample
+import androidx.compose.material3.samples.HorizontalUncontainedCarouselSample
import androidx.compose.material3.samples.IconButtonSample
import androidx.compose.material3.samples.IconToggleButtonSample
import androidx.compose.material3.samples.IndeterminateCircularProgressIndicatorSample
@@ -326,11 +327,18 @@
private const val CarouselExampleSourceUrl = "$SampleSourceUrl/CarouselSamples.kt"
val CarouselExamples = listOf(
Example(
- name = ::CarouselSample.name,
+ name = ::HorizontalMultiBrowseCarouselSample.name,
description = CarouselExampleDescription,
sourceUrl = CarouselExampleSourceUrl
) {
- CarouselSample()
+ HorizontalMultiBrowseCarouselSample()
+ },
+ Example(
+ name = ::HorizontalUncontainedCarouselSample.name,
+ description = CarouselExampleDescription,
+ sourceUrl = CarouselExampleSourceUrl
+ ) {
+ HorizontalUncontainedCarouselSample()
}
)
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
index a55a9bd..5410945 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
@@ -17,16 +17,18 @@
package androidx.compose.material3.samples
import androidx.annotation.DrawableRes
+import androidx.annotation.Sampled
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Card
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel
+import androidx.compose.material3.carousel.HorizontalUncontainedCarousel
+import androidx.compose.material3.carousel.rememberCarouselState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
@@ -35,16 +37,19 @@
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+@OptIn(ExperimentalMaterial3Api::class)
@Preview
+@Sampled
@Composable
-fun CarouselSample() {
+fun HorizontalMultiBrowseCarouselSample() {
+
data class CarouselItem(
val id: Int,
@DrawableRes val imageResId: Int,
@StringRes val contentDescriptionResId: Int
)
- val Items = listOf(
+ val items = listOf(
CarouselItem(0, R.drawable.carousel_image_1, R.string.carousel_image_1_description),
CarouselItem(1, R.drawable.carousel_image_2, R.string.carousel_image_2_description),
CarouselItem(2, R.drawable.carousel_image_3, R.string.carousel_image_3_description),
@@ -52,23 +57,69 @@
CarouselItem(4, R.drawable.carousel_image_5, R.string.carousel_image_5_description),
)
- LazyRow(
- modifier = Modifier.fillMaxWidth(),
- state = rememberLazyListState()
- ) {
- itemsIndexed(Items) { _, item ->
- Card(
- modifier = Modifier
- .width(350.dp)
- .height(200.dp),
- ) {
- Image(
- painter = painterResource(id = item.imageResId),
- contentDescription = stringResource(item.contentDescriptionResId),
- modifier = Modifier.fillMaxSize(),
- contentScale = ContentScale.Crop
- )
- }
+ HorizontalMultiBrowseCarousel(
+ state = rememberCarouselState { items.count() },
+ modifier = Modifier
+ .width(412.dp)
+ .height(221.dp),
+ preferredItemWidth = 186.dp,
+ itemSpacing = 8.dp,
+ contentPadding = PaddingValues(horizontal = 16.dp)
+ ) { i ->
+ val item = items[i]
+ Card(
+ modifier = Modifier
+ .height(205.dp)
+ ) {
+ Image(
+ painter = painterResource(id = item.imageResId),
+ contentDescription = stringResource(item.contentDescriptionResId),
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Sampled
+@Composable
+fun HorizontalUncontainedCarouselSample() {
+
+ data class CarouselItem(
+ val id: Int,
+ @DrawableRes val imageResId: Int,
+ @StringRes val contentDescriptionResId: Int
+ )
+
+ val items = listOf(
+ CarouselItem(0, R.drawable.carousel_image_1, R.string.carousel_image_1_description),
+ CarouselItem(1, R.drawable.carousel_image_2, R.string.carousel_image_2_description),
+ CarouselItem(2, R.drawable.carousel_image_3, R.string.carousel_image_3_description),
+ CarouselItem(3, R.drawable.carousel_image_4, R.string.carousel_image_4_description),
+ CarouselItem(4, R.drawable.carousel_image_5, R.string.carousel_image_5_description),
+ )
+ HorizontalUncontainedCarousel(
+ state = rememberCarouselState { items.count() },
+ modifier = Modifier
+ .width(412.dp)
+ .height(221.dp),
+ itemWidth = 186.dp,
+ itemSpacing = 8.dp,
+ contentPadding = PaddingValues(horizontal = 16.dp)
+ ) { i ->
+ val item = items[i]
+ Card(
+ modifier = Modifier
+ .height(205.dp)
+ ) {
+ Image(
+ painter = painterResource(id = item.imageResId),
+ contentDescription = stringResource(item.contentDescriptionResId),
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
}
}
}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DrawerSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DrawerSamples.kt
index f21529e..6d07a51 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DrawerSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/DrawerSamples.kt
@@ -16,7 +16,6 @@
package androidx.compose.material3.samples
-import androidx.activity.compose.BackHandler
import androidx.annotation.Sampled
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -24,10 +23,27 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.Bookmarks
+import androidx.compose.material.icons.filled.CalendarMonth
+import androidx.compose.material.icons.filled.Dashboard
import androidx.compose.material.icons.filled.Email
-import androidx.compose.material.icons.filled.Face
import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Group
+import androidx.compose.material.icons.filled.Headphones
+import androidx.compose.material.icons.filled.Image
+import androidx.compose.material.icons.filled.JoinFull
+import androidx.compose.material.icons.filled.Keyboard
+import androidx.compose.material.icons.filled.Laptop
+import androidx.compose.material.icons.filled.Map
+import androidx.compose.material.icons.filled.Navigation
+import androidx.compose.material.icons.filled.Outbox
+import androidx.compose.material.icons.filled.PushPin
+import androidx.compose.material.icons.filled.QrCode
+import androidx.compose.material.icons.filled.Radio
import androidx.compose.material3.Button
import androidx.compose.material3.DismissibleDrawerSheet
import androidx.compose.material3.DismissibleNavigationDrawer
@@ -58,24 +74,45 @@
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
// icons to mimic drawer destinations
- val items = listOf(Icons.Default.Favorite, Icons.Default.Face, Icons.Default.Email)
+ val items = listOf(
+ Icons.Default.AccountCircle,
+ Icons.Default.Bookmarks,
+ Icons.Default.CalendarMonth,
+ Icons.Default.Dashboard,
+ Icons.Default.Email,
+ Icons.Default.Favorite,
+ Icons.Default.Group,
+ Icons.Default.Headphones,
+ Icons.Default.Image,
+ Icons.Default.JoinFull,
+ Icons.Default.Keyboard,
+ Icons.Default.Laptop,
+ Icons.Default.Map,
+ Icons.Default.Navigation,
+ Icons.Default.Outbox,
+ Icons.Default.PushPin,
+ Icons.Default.QrCode,
+ Icons.Default.Radio,
+ )
val selectedItem = remember { mutableStateOf(items[0]) }
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
- ModalDrawerSheet {
- Spacer(Modifier.height(12.dp))
- items.forEach { item ->
- NavigationDrawerItem(
- icon = { Icon(item, contentDescription = null) },
- label = { Text(item.name) },
- selected = item == selectedItem.value,
- onClick = {
- scope.launch { drawerState.close() }
- selectedItem.value = item
- },
- modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
- )
+ ModalDrawerSheet(drawerState) {
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ Spacer(Modifier.height(12.dp))
+ items.forEach { item ->
+ NavigationDrawerItem(
+ icon = { Icon(item, contentDescription = null) },
+ label = { Text(item.name) },
+ selected = item == selectedItem.value,
+ onClick = {
+ scope.launch { drawerState.close() }
+ selectedItem.value = item
+ },
+ modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
+ )
+ }
}
}
},
@@ -101,22 +138,43 @@
@Composable
fun PermanentNavigationDrawerSample() {
// icons to mimic drawer destinations
- val items = listOf(Icons.Default.Favorite, Icons.Default.Face, Icons.Default.Email)
+ val items = listOf(
+ Icons.Default.AccountCircle,
+ Icons.Default.Bookmarks,
+ Icons.Default.CalendarMonth,
+ Icons.Default.Dashboard,
+ Icons.Default.Email,
+ Icons.Default.Favorite,
+ Icons.Default.Group,
+ Icons.Default.Headphones,
+ Icons.Default.Image,
+ Icons.Default.JoinFull,
+ Icons.Default.Keyboard,
+ Icons.Default.Laptop,
+ Icons.Default.Map,
+ Icons.Default.Navigation,
+ Icons.Default.Outbox,
+ Icons.Default.PushPin,
+ Icons.Default.QrCode,
+ Icons.Default.Radio,
+ )
val selectedItem = remember { mutableStateOf(items[0]) }
PermanentNavigationDrawer(
drawerContent = {
PermanentDrawerSheet(Modifier.width(240.dp)) {
- Spacer(Modifier.height(12.dp))
- items.forEach { item ->
- NavigationDrawerItem(
- icon = { Icon(item, contentDescription = null) },
- label = { Text(item.name) },
- selected = item == selectedItem.value,
- onClick = {
- selectedItem.value = item
- },
- modifier = Modifier.padding(horizontal = 12.dp)
- )
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ Spacer(Modifier.height(12.dp))
+ items.forEach { item ->
+ NavigationDrawerItem(
+ icon = { Icon(item, contentDescription = null) },
+ label = { Text(item.name) },
+ selected = item == selectedItem.value,
+ onClick = {
+ selectedItem.value = item
+ },
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+ }
}
}
},
@@ -140,30 +198,46 @@
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
// icons to mimic drawer destinations
- val items = listOf(Icons.Default.Favorite, Icons.Default.Face, Icons.Default.Email)
+ val items = listOf(
+ Icons.Default.AccountCircle,
+ Icons.Default.Bookmarks,
+ Icons.Default.CalendarMonth,
+ Icons.Default.Dashboard,
+ Icons.Default.Email,
+ Icons.Default.Favorite,
+ Icons.Default.Group,
+ Icons.Default.Headphones,
+ Icons.Default.Image,
+ Icons.Default.JoinFull,
+ Icons.Default.Keyboard,
+ Icons.Default.Laptop,
+ Icons.Default.Map,
+ Icons.Default.Navigation,
+ Icons.Default.Outbox,
+ Icons.Default.PushPin,
+ Icons.Default.QrCode,
+ Icons.Default.Radio,
+ )
val selectedItem = remember { mutableStateOf(items[0]) }
- BackHandler(enabled = drawerState.isOpen) {
- scope.launch {
- drawerState.close()
- }
- }
DismissibleNavigationDrawer(
drawerState = drawerState,
drawerContent = {
- DismissibleDrawerSheet {
- Spacer(Modifier.height(12.dp))
- items.forEach { item ->
- NavigationDrawerItem(
- icon = { Icon(item, contentDescription = null) },
- label = { Text(item.name) },
- selected = item == selectedItem.value,
- onClick = {
- scope.launch { drawerState.close() }
- selectedItem.value = item
- },
- modifier = Modifier.padding(horizontal = 12.dp)
- )
+ DismissibleDrawerSheet(drawerState) {
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ Spacer(Modifier.height(12.dp))
+ items.forEach { item ->
+ NavigationDrawerItem(
+ icon = { Icon(item, contentDescription = null) },
+ label = { Text(item.name) },
+ selected = item == selectedItem.value,
+ onClick = {
+ scope.launch { drawerState.close() }
+ selectedItem.value = item
+ },
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+ }
}
}
},
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerScreenshotTest.kt
index 06d8653..3abf32b 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerScreenshotTest.kt
@@ -19,11 +19,34 @@
import android.os.Build
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.Build
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.DateRange
+import androidx.compose.material.icons.filled.Email
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.Lock
+import androidx.compose.material.icons.filled.Notifications
+import androidx.compose.material.icons.filled.Place
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.ShoppingCart
+import androidx.compose.material.icons.filled.ThumbUp
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.runtime.Composable
import androidx.compose.testutils.assertAgainstGolden
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.ComposeContentTestRule
@@ -117,6 +140,167 @@
.captureToImage()
.assertAgainstGolden(screenshotRule, goldenName)
}
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress0AndSwipeEdgeLeft() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0f, swipeEdgeLeft = true)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress0AndSwipeEdgeLeft")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress25AndSwipeEdgeLeft() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0.25f, swipeEdgeLeft = true)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress25AndSwipeEdgeLeft")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress50AndSwipeEdgeLeft() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0.5f, swipeEdgeLeft = true)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress50AndSwipeEdgeLeft")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress75AndSwipeEdgeLeft() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0.75f, swipeEdgeLeft = true)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress75AndSwipeEdgeLeft")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress100AndSwipeEdgeLeft() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 1f, swipeEdgeLeft = true)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress100AndSwipeEdgeLeft")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress0AndSwipeEdgeRight() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0f, swipeEdgeLeft = false)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress0AndSwipeEdgeRight")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress25AndSwipeEdgeRight() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0.25f, swipeEdgeLeft = false)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress25AndSwipeEdgeRight")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress50AndSwipeEdgeRight() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0.5f, swipeEdgeLeft = false)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress50AndSwipeEdgeRight")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress75AndSwipeEdgeRight() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 0.75f, swipeEdgeLeft = false)
+ }
+ assertScreenshotAgainstGolden("navigationDrawer_predictiveBack_progress75AndSwipeEdgeRight")
+ }
+
+ @Test
+ fun predictiveBack_navigationDrawer_progress100AndSwipeEdgeRight() {
+ rule.setMaterialContent(lightColorScheme()) {
+ ModalNavigationDrawerPredictiveBack(progress = 1f, swipeEdgeLeft = false)
+ }
+ assertScreenshotAgainstGolden(
+ "navigationDrawer_predictiveBack_progress100AndSwipeEdgeRight"
+ )
+ }
}
private val ContainerTestTag = "container"
+
+private val items = listOf(
+ Icons.Default.AccountCircle,
+ Icons.Default.Build,
+ Icons.Default.Check,
+ Icons.Default.DateRange,
+ Icons.Default.Email,
+ Icons.Default.Favorite,
+ Icons.Default.Home,
+ Icons.Default.Info,
+ Icons.Default.Lock,
+ Icons.Default.Notifications,
+ Icons.Default.Place,
+ Icons.Default.Refresh,
+ Icons.Default.ShoppingCart,
+ Icons.Default.ThumbUp,
+ Icons.Default.Warning,
+)
+
+@Composable
+private fun ModalNavigationDrawerPredictiveBack(progress: Float, swipeEdgeLeft: Boolean) {
+ val maxScaleXDistanceGrow: Float
+ val maxScaleXDistanceShrink: Float
+ val maxScaleYDistance: Float
+ with(LocalDensity.current) {
+ maxScaleXDistanceGrow = PredictiveBackDrawerMaxScaleXDistanceGrow.toPx()
+ maxScaleXDistanceShrink = PredictiveBackDrawerMaxScaleXDistanceShrink.toPx()
+ maxScaleYDistance = PredictiveBackDrawerMaxScaleYDistance.toPx()
+ }
+
+ val drawerPredictiveBackState = DrawerPredictiveBackState().apply {
+ update(
+ progress = progress,
+ swipeEdgeLeft = swipeEdgeLeft,
+ isRtl = false,
+ maxScaleXDistanceGrow = maxScaleXDistanceGrow,
+ maxScaleXDistanceShrink = maxScaleXDistanceShrink,
+ maxScaleYDistance = maxScaleYDistance
+ )
+ }
+
+ ModalNavigationDrawer(
+ modifier = Modifier.testTag(ContainerTestTag),
+ drawerState = rememberDrawerState(DrawerValue.Open),
+ drawerContent = {
+ // Use the internal DrawerSheet instead of ModalDrawerSheet so we can simulate different
+ // back progress values for the test, and avoid the real PredictiveBackHandler.
+ DrawerSheet(
+ drawerPredictiveBackState,
+ DrawerDefaults.windowInsets,
+ Modifier,
+ DrawerDefaults.shape,
+ DrawerDefaults.modalContainerColor,
+ contentColorFor(DrawerDefaults.modalContainerColor),
+ DrawerDefaults.ModalDrawerElevation
+ ) {
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ Spacer(Modifier.height(12.dp))
+ items.forEach { item ->
+ NavigationDrawerItem(
+ icon = { Icon(item, contentDescription = null) },
+ label = { Text(item.name) },
+ selected = item == Icons.Default.AccountCircle,
+ onClick = {},
+ modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
+ )
+ }
+ }
+ }
+ },
+ content = {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)
+ )
+ }
+ )
+}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
index 80e6eee..75d56ed 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
@@ -21,6 +21,7 @@
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.TargetedFlingBehavior
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
@@ -193,9 +194,12 @@
add(smallSize)
add(xSmallSize, isAnchor = true)
}
- val strategy = Strategy { _, _ ->
- keylineList
- }.apply(availableSpace = 1000f, itemSpacing = 0f)
+ val strategy = Strategy { _, _ -> keylineList }.apply(
+ availableSpace = 1000f,
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
val outOfBoundsNum = calculateOutOfBounds(strategy)
// With this strategy, we expect 3 loaded items
val loadedItems = 3
@@ -219,7 +223,12 @@
add(56f)
add(10f, isAnchor = true)
}
- }.apply(availableSpace = 380f, itemSpacing = 8f)
+ }.apply(
+ availableSpace = 380f,
+ itemSpacing = 8f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
// Max offset should only add item spacing between each item
val expectedMaxScrollOffset = (186f * 10) + (8f * 9) - 380f
@@ -278,6 +287,7 @@
flingBehavior = flingBehavior(state),
modifier = modifier.testTag(CarouselTestTag),
itemSpacing = 0.dp,
+ contentPadding = PaddingValues(0.dp),
content = content,
)
}
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/NavigationDrawer.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/NavigationDrawer.android.kt
new file mode 100644
index 0000000..535c610
--- /dev/null
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/NavigationDrawer.android.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * 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 androidx.compose.material3
+
+import androidx.activity.BackEventCompat
+import androidx.activity.compose.PredictiveBackHandler
+import androidx.compose.animation.core.animate
+import androidx.compose.material3.internal.PredictiveBack
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import kotlin.coroutines.cancellation.CancellationException
+import kotlinx.coroutines.launch
+
+/**
+ * Registers a [PredictiveBackHandler] and provides animation values in [DrawerPredictiveBackState]
+ * based on back progress.
+ *
+ * @param drawerState state of the drawer
+ * @param content content of the rest of the UI
+ */
+@Composable
+internal actual fun DrawerPredictiveBackHandler(
+ drawerState: DrawerState,
+ content: @Composable (DrawerPredictiveBackState) -> Unit
+) {
+ val drawerPredictiveBackState = remember { DrawerPredictiveBackState() }
+ val scope = rememberCoroutineScope()
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ val maxScaleXDistanceGrow: Float
+ val maxScaleXDistanceShrink: Float
+ val maxScaleYDistance: Float
+ with(LocalDensity.current) {
+ maxScaleXDistanceGrow = PredictiveBackDrawerMaxScaleXDistanceGrow.toPx()
+ maxScaleXDistanceShrink = PredictiveBackDrawerMaxScaleXDistanceShrink.toPx()
+ maxScaleYDistance = PredictiveBackDrawerMaxScaleYDistance.toPx()
+ }
+
+ PredictiveBackHandler(enabled = drawerState.isOpen) { progress ->
+ try {
+ progress.collect { backEvent ->
+ drawerPredictiveBackState.update(
+ PredictiveBack.transform(backEvent.progress),
+ backEvent.swipeEdge == BackEventCompat.EDGE_LEFT,
+ isRtl,
+ maxScaleXDistanceGrow,
+ maxScaleXDistanceShrink,
+ maxScaleYDistance
+ )
+ }
+ } catch (e: CancellationException) {
+ drawerPredictiveBackState.clear()
+ } finally {
+ if (drawerPredictiveBackState.swipeEdgeMatchesDrawer) {
+ // If swipe edge matches drawer gravity and we've stretched the drawer horizontally,
+ // un-stretch it smoothly so that it hides completely during the drawer close.
+ scope.launch {
+ animate(
+ initialValue = drawerPredictiveBackState.scaleXDistance,
+ targetValue = 0f
+ ) { value, _ -> drawerPredictiveBackState.scaleXDistance = value }
+ drawerPredictiveBackState.clear()
+ }
+ }
+ drawerState.close()
+ }
+ }
+
+ LaunchedEffect(drawerState.isClosed) {
+ if (drawerState.isClosed) {
+ drawerPredictiveBackState.clear()
+ }
+ }
+
+ content(drawerPredictiveBackState)
+}
+
+internal val PredictiveBackDrawerMaxScaleXDistanceGrow = 12.dp
+internal val PredictiveBackDrawerMaxScaleXDistanceShrink = 24.dp
+internal val PredictiveBackDrawerMaxScaleYDistance = 48.dp
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt
index 9e32c8e..a6b9613 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/KeylineSnapPositionTest.kt
@@ -97,7 +97,12 @@
add(xSmallSize, isAnchor = true)
}
- return Strategy { _, _ -> keylineList }.apply(availableSpace = 1000f, itemSpacing = 0f)
+ return Strategy { _, _ -> keylineList }.apply(
+ availableSpace = 1000f,
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
}
// Test strategy that is start aligned:
@@ -127,7 +132,12 @@
add(smallSize)
add(xSmallSize, isAnchor = true)
}
- return Strategy { _, _ -> keylineList }.apply(availableSpace = 1000f, itemSpacing = 0f)
+ return Strategy { _, _ -> keylineList }.apply(
+ availableSpace = 1000f,
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
}
// Test strategy that is start aligned:
@@ -150,6 +160,11 @@
add(smallSize)
add(xSmallSize, isAnchor = true)
}
- return Strategy { _, _ -> keylineList }.apply(1000f, 0f)
+ return Strategy { _, _ -> keylineList }.apply(
+ availableSpace = 1000f,
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
}
}
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
index 84738c7..830559f 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
@@ -39,7 +39,12 @@
itemSpacing = 0f,
itemCount = 10,
)!!
- val strategy = Strategy { _, _ -> keylineList }.apply(500f, 0f)
+ val strategy = Strategy { _, _ -> keylineList }.apply(
+ availableSpace = 500f,
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
assertThat(strategy.itemMainAxisSize).isEqualTo(itemSize)
}
@@ -53,10 +58,12 @@
preferredItemSize = itemSize,
itemSpacing = 0f,
itemCount = 10,
- )!!
+ )!!
val strategy = Strategy { _, _ -> keylineList }.apply(
availableSpace = 100f,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val minSmallItemSize: Float = with(Density) { CarouselDefaults.MinSmallItemSize.toPx() }
val keylines = strategy.defaultKeylines
@@ -80,10 +87,12 @@
preferredItemSize = 200f,
itemSpacing = 0f,
itemCount = 10,
- )!!
+ )!!
val strategy = Strategy { _, _ -> keylineList }.apply(
availableSpace = minSmallItemSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val keylines = strategy.defaultKeylines
@@ -118,7 +127,9 @@
)!!
val strategy = Strategy { _, _ -> keylineList }.apply(
availableSpace = carouselSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val keylines = strategy.defaultKeylines
@@ -144,7 +155,12 @@
itemSpacing = 0f,
itemCount = 3,
)!!
- val strategy = Strategy { _, _ -> keylineList }.apply(carouselSize, 0f)
+ val strategy = Strategy { _, _ -> keylineList }.apply(
+ availableSpace = carouselSize,
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
val keylines = strategy.defaultKeylines
// We originally expect a keyline list of [xSmall-Large-Large-Medium-Small-xSmall], but with
@@ -155,6 +171,7 @@
assertThat(keylines[3].size).isLessThan(keylines[2].size)
}
+ @Test
fun testMultiBrowse_adjustsForItemSpacing() {
val keylineList = multiBrowseKeylineList(
density = Density,
@@ -163,7 +180,12 @@
itemSpacing = 8f,
itemCount = 10
)!!
- val strategy = Strategy { _, _ -> keylineList }.apply(380f, 8f)
+ val strategy = Strategy { _, _ -> keylineList }.apply(
+ availableSpace = 380f,
+ itemSpacing = 8f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
assertThat(keylineList.firstFocal.size).isEqualTo(186f)
// Ensure the first visible item is large and aligned with the start of the container
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
index 06052ff..595e34c 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/StrategyTest.kt
@@ -36,7 +36,9 @@
val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
availableSpace = carouselMainAxisSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
assertThat(strategy.getKeylineListForScrollOffset(0f, maxScrollOffset))
@@ -67,7 +69,9 @@
val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
availableSpace = carouselMainAxisSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val endKeylineList = strategy.getEndKeylines()
@@ -89,7 +93,9 @@
val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
availableSpace = carouselMainAxisSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val startKeylineList = strategy.getStartKeylines()
@@ -112,7 +118,9 @@
val strategy = Strategy { _, _ -> defaultKeylines }.apply(
availableSpace = carouselMainAxisSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val startSteps = listOf(
@@ -219,7 +227,9 @@
val strategy = Strategy { _, _ -> defaultKeylines }.apply(
availableSpace = carouselMainAxisSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val endSteps = listOf(
@@ -354,8 +364,18 @@
val strategy2 = Strategy { availableSpace, itemSpacingPx ->
multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
}
- strategy1.apply(availableSpace = 500f, itemSpacing = itemSpacing)
- strategy2.apply(availableSpace = 500f, itemSpacing = itemSpacing)
+ strategy1.apply(
+ availableSpace = 500f,
+ itemSpacing = itemSpacing,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
+ strategy2.apply(
+ availableSpace = 500f,
+ itemSpacing = itemSpacing,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
assertThat(strategy1 == strategy2).isTrue()
assertThat(strategy1.hashCode()).isEqualTo(strategy2.hashCode())
@@ -372,8 +392,18 @@
val strategy2 = Strategy { availableSpace, itemSpacingPx ->
multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
}
- strategy1.apply(availableSpace = 500f, itemSpacing = itemSpacing)
- strategy2.apply(availableSpace = 500f + 1f, itemSpacing = itemSpacing)
+ strategy1.apply(
+ availableSpace = 500f,
+ itemSpacing = itemSpacing,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
+ strategy2.apply(
+ availableSpace = 500f + 1f,
+ itemSpacing = itemSpacing,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
assertThat(strategy1 == strategy2).isFalse()
assertThat(strategy1.hashCode()).isNotEqualTo(strategy2.hashCode())
@@ -390,7 +420,12 @@
val strategy2 = Strategy { availableSpace, itemSpacingPx ->
multiBrowseKeylineList(Density, availableSpace, itemSize, itemSpacingPx, itemCount)
}
- strategy1.apply(availableSpace = 500f, itemSpacing = itemSpacing)
+ strategy1.apply(
+ availableSpace = 500f,
+ itemSpacing = itemSpacing,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
assertThat(strategy1 == strategy2).isFalse()
assertThat(strategy1.hashCode()).isNotEqualTo(strategy2.hashCode())
@@ -403,7 +438,12 @@
val maxScrollOffset = (itemCount * large) - carouselMainAxisSize
val defaultKeylineList = createStartAlignedKeylineList()
- val strategy = Strategy { _, _ -> defaultKeylineList }.apply(carouselMainAxisSize, 0f)
+ val strategy = Strategy { _, _ -> defaultKeylineList }.apply(
+ availableSpace = carouselMainAxisSize,
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
assertThat(strategy.getKeylineListForScrollOffset(0f, maxScrollOffset))
.isEqualTo(defaultKeylineList)
@@ -419,7 +459,12 @@
add(56f)
add(10f, isAnchor = true)
}
- }.apply(availableSpace = 380f, itemSpacing = 8f)
+ }.apply(
+ availableSpace = 380f,
+ itemSpacing = 8f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
val middleStep = strategy.endKeylineSteps[1]
val actualMiddleOffsets = middleStep.map { it.offset }.toFloatArray()
@@ -453,7 +498,12 @@
add(56f)
add(10f, isAnchor = true)
}
- }.apply(availableSpace = 768f, itemSpacing = 8f)
+ }.apply(
+ availableSpace = 768f,
+ itemSpacing = 8f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
+ )
assertThat(strategy.startKeylineSteps).hasSize(3)
assertThat(strategy.endKeylineSteps).hasSize(3)
@@ -507,6 +557,38 @@
assertThat(e2ActualUnadjustedOffsets).isEqualTo(e2ExpectedUnadjustedOffsets)
}
+ @Test
+ fun testCenterStrategy_stepsShouldAccountForContentPadding() {
+ val strategy = Strategy { availableSpace, itemSpacing ->
+ keylineListOf(availableSpace, itemSpacing, CarouselAlignment.Center) {
+ add(10f, isAnchor = true)
+ add(50f)
+ add(100f)
+ add(200f)
+ add(100f)
+ add(50f)
+ add(10f, isAnchor = true)
+ }
+ }.apply(500f, 0f, 16f, 24f)
+
+ val lastStartStep = strategy.startKeylineSteps.last()
+
+ val firstFocalLeft = lastStartStep.firstFocal.offset -
+ (lastStartStep.firstFocal.size / 2f)
+ val lastNonAnchorRight = lastStartStep.lastNonAnchor.offset +
+ (lastStartStep.lastNonAnchor.size / 2f)
+ val lastEndStep = strategy.endKeylineSteps.last()
+ val lastFocalRight = lastEndStep.lastFocal.offset +
+ (lastEndStep.lastFocal.size / 2f)
+ val firstNonAnchorLeft = lastEndStep.firstNonAnchor.offset -
+ (lastEndStep.firstNonAnchor.size / 2f)
+
+ assertThat(firstFocalLeft).isEqualTo(16f)
+ assertThat(lastNonAnchorRight).isEqualTo(500f)
+ assertThat(lastFocalRight).isEqualTo(500f - 24f)
+ assertThat(firstNonAnchorLeft).isWithin(.01f).of(0f)
+ }
+
private fun assertEqualWithFloatTolerance(
tolerance: Float,
actual: Keyline,
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
index 9f7ba239..cf6aa49 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
@@ -41,7 +41,9 @@
)
val strategy = Strategy { _, _ -> keylineList }.apply(
availableSpace = carouselSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val keylines = strategy.defaultKeylines
val anchorSize = with(Density) { CarouselDefaults.AnchorSize.toPx() }
@@ -67,7 +69,9 @@
)
val strategy = Strategy { _, _ -> keylineList }.apply(
availableSpace = carouselSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val keylines = strategy.defaultKeylines
val anchorSize = with(Density) { CarouselDefaults.AnchorSize.toPx() }
@@ -96,7 +100,9 @@
)
val strategy = Strategy { _, _ -> keylineList }.apply(
availableSpace = carouselSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val keylines = strategy.defaultKeylines
val rightAnchorSize = with(Density) { CarouselDefaults.AnchorSize.toPx() }
@@ -134,7 +140,9 @@
)
val strategy = Strategy { _, _ -> keylineList }.apply(
availableSpace = carouselSize,
- itemSpacing = 0f
+ itemSpacing = 0f,
+ beforeContentPadding = 0f,
+ afterContentPadding = 0f
)
val keylines = strategy.defaultKeylines
val rightAnchorSize = with(Density) { CarouselDefaults.AnchorSize.toPx() }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
index ca40239..24bdd24 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
@@ -51,6 +51,7 @@
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -61,8 +62,11 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
@@ -79,6 +83,7 @@
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.lerp
import kotlin.math.roundToInt
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
@@ -510,6 +515,10 @@
/**
* Content inside of a modal navigation drawer.
+
+ * Note: This version of [ModalDrawerSheet] does not handle back by default. For automatic back
+ * handling and predictive back animations on Android 14+, use the [ModalDrawerSheet] that accepts
+ * `drawerState` as a param.
*
* @param modifier the [Modifier] to be applied to this drawer's content
* @param drawerShape defines the shape of this drawer's container
@@ -535,6 +544,7 @@
content: @Composable ColumnScope.() -> Unit
) {
DrawerSheet(
+ drawerPredictiveBackState = null,
windowInsets,
modifier,
drawerShape,
@@ -546,8 +556,58 @@
}
/**
+ * Content inside of a modal navigation drawer.
+ *
+ * Note: This version of [ModalDrawerSheet] requires a [drawerState] to be provided and will handle
+ * back by default for all Android versions, as well as animate during predictive back on Android
+ * 14+.
+ *
+ * @param drawerState state of the drawer
+ * @param modifier the [Modifier] to be applied to this drawer's content
+ * @param drawerShape defines the shape of this drawer's container
+ * @param drawerContainerColor the color used for the background of this drawer. Use
+ * [Color.Transparent] to have no color.
+ * @param drawerContentColor the preferred color for content inside this drawer. Defaults to either
+ * the matching content color for [drawerContainerColor], or to the current [LocalContentColor] if
+ * [drawerContainerColor] is not a color from the theme.
+ * @param drawerTonalElevation when [drawerContainerColor] is [ColorScheme.surface], a translucent
+ * primary color overlay is applied on top of the container. A higher tonal elevation value will
+ * result in a darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param windowInsets a window insets for the sheet.
+ * @param content content inside of a modal navigation drawer
+ */
+@Composable
+fun ModalDrawerSheet(
+ drawerState: DrawerState,
+ modifier: Modifier = Modifier,
+ drawerShape: Shape = DrawerDefaults.shape,
+ drawerContainerColor: Color = DrawerDefaults.modalContainerColor,
+ drawerContentColor: Color = contentColorFor(drawerContainerColor),
+ drawerTonalElevation: Dp = DrawerDefaults.ModalDrawerElevation,
+ windowInsets: WindowInsets = DrawerDefaults.windowInsets,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ DrawerPredictiveBackHandler(drawerState) { drawerPredictiveBackState ->
+ DrawerSheet(
+ drawerPredictiveBackState,
+ windowInsets,
+ modifier,
+ drawerShape,
+ drawerContainerColor,
+ drawerContentColor,
+ drawerTonalElevation,
+ content
+ )
+ }
+}
+
+/**
* Content inside of a dismissible navigation drawer.
*
+ * Note: This version of [DismissibleDrawerSheet] does not handle back by default. For automatic
+ * back handling and predictive back animations on Android 14+, use the [DismissibleDrawerSheet]
+ * that accepts `drawerState` as a param.
+ *
* @param modifier the [Modifier] to be applied to this drawer's content
* @param drawerShape defines the shape of this drawer's container
* @param drawerContainerColor the color used for the background of this drawer. Use
@@ -572,6 +632,7 @@
content: @Composable ColumnScope.() -> Unit
) {
DrawerSheet(
+ drawerPredictiveBackState = null,
windowInsets,
modifier,
drawerShape,
@@ -583,6 +644,52 @@
}
/**
+ * Content inside of a dismissible navigation drawer.
+
+ * Note: This version of [DismissibleDrawerSheet] requires a [drawerState] to be provided and will
+ * handle back by default for all Android versions, as well as animate during predictive back on
+ * Android 14+.
+ *
+ * @param drawerState state of the drawer
+ * @param modifier the [Modifier] to be applied to this drawer's content
+ * @param drawerShape defines the shape of this drawer's container
+ * @param drawerContainerColor the color used for the background of this drawer. Use
+ * [Color.Transparent] to have no color.
+ * @param drawerContentColor the preferred color for content inside this drawer. Defaults to either
+ * the matching content color for [drawerContainerColor], or to the current [LocalContentColor] if
+ * [drawerContainerColor] is not a color from the theme.
+ * @param drawerTonalElevation when [drawerContainerColor] is [ColorScheme.surface], a translucent
+ * primary color overlay is applied on top of the container. A higher tonal elevation value will
+ * result in a darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param windowInsets a window insets for the sheet.
+ * @param content content inside of a dismissible navigation drawer
+ */
+@Composable
+fun DismissibleDrawerSheet(
+ drawerState: DrawerState,
+ modifier: Modifier = Modifier,
+ drawerShape: Shape = RectangleShape,
+ drawerContainerColor: Color = DrawerDefaults.standardContainerColor,
+ drawerContentColor: Color = contentColorFor(drawerContainerColor),
+ drawerTonalElevation: Dp = DrawerDefaults.DismissibleDrawerElevation,
+ windowInsets: WindowInsets = DrawerDefaults.windowInsets,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ DrawerPredictiveBackHandler(drawerState) { drawerPredictiveBackState ->
+ DrawerSheet(
+ drawerPredictiveBackState,
+ windowInsets,
+ modifier,
+ drawerShape,
+ drawerContainerColor,
+ drawerContentColor,
+ drawerTonalElevation,
+ content
+ )
+ }
+}
+
+/**
* Content inside of a permanent navigation drawer.
*
* @param modifier the [Modifier] to be applied to this drawer's content
@@ -610,6 +717,7 @@
) {
val navigationMenu = getString(Strings.NavigationMenu)
DrawerSheet(
+ drawerPredictiveBackState = null,
windowInsets,
modifier.semantics {
paneTitle = navigationMenu
@@ -623,7 +731,8 @@
}
@Composable
-private fun DrawerSheet(
+internal fun DrawerSheet(
+ drawerPredictiveBackState: DrawerPredictiveBackState?,
windowInsets: WindowInsets,
modifier: Modifier = Modifier,
drawerShape: Shape = RectangleShape,
@@ -632,30 +741,93 @@
drawerTonalElevation: Dp = DrawerDefaults.PermanentDrawerElevation,
content: @Composable ColumnScope.() -> Unit
) {
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ val predictiveBackDrawerContainerModifier =
+ if (drawerPredictiveBackState != null) Modifier.predictiveBackDrawerContainer(
+ drawerPredictiveBackState,
+ isRtl
+ ) else Modifier
Surface(
modifier = modifier
.sizeIn(
minWidth = MinimumDrawerWidth,
maxWidth = DrawerDefaults.MaximumDrawerWidth
)
+ .then(predictiveBackDrawerContainerModifier)
.fillMaxHeight(),
shape = drawerShape,
color = drawerContainerColor,
contentColor = drawerContentColor,
tonalElevation = drawerTonalElevation
) {
+ val predictiveBackDrawerChildModifier =
+ if (drawerPredictiveBackState != null) Modifier.predictiveBackDrawerChild(
+ drawerPredictiveBackState,
+ isRtl
+ ) else Modifier
Column(
Modifier
.sizeIn(
minWidth = MinimumDrawerWidth,
maxWidth = DrawerDefaults.MaximumDrawerWidth
)
+ .then(predictiveBackDrawerChildModifier)
.windowInsetsPadding(windowInsets),
content = content
)
}
}
+private fun Modifier.predictiveBackDrawerContainer(
+ drawerPredictiveBackState: DrawerPredictiveBackState,
+ isRtl: Boolean
+) = graphicsLayer {
+ scaleX = calculatePredictiveBackScaleX(drawerPredictiveBackState)
+ scaleY = calculatePredictiveBackScaleY(drawerPredictiveBackState)
+ transformOrigin = TransformOrigin(if (isRtl) 1f else 0f, 0.5f)
+}
+
+private fun Modifier.predictiveBackDrawerChild(
+ drawerPredictiveBackState: DrawerPredictiveBackState,
+ isRtl: Boolean
+) = graphicsLayer {
+ // Preserve the original aspect ratio and container alignment of the child
+ // content, and add content margins.
+ val containerScaleX = calculatePredictiveBackScaleX(drawerPredictiveBackState)
+ val containerScaleY = calculatePredictiveBackScaleY(drawerPredictiveBackState)
+ scaleX = if (containerScaleX != 0f) containerScaleY / containerScaleX else 1f
+ transformOrigin = TransformOrigin(if (isRtl) 0f else 1f, 0f)
+}
+
+private fun GraphicsLayerScope.calculatePredictiveBackScaleX(
+ drawerPredictiveBackState: DrawerPredictiveBackState
+): Float {
+ val width = size.width
+ return if (width.isNaN() || width == 0f) {
+ 1f
+ } else {
+ val scaleXDirection = if (drawerPredictiveBackState.swipeEdgeMatchesDrawer) 1 else -1
+ 1f + drawerPredictiveBackState.scaleXDistance * scaleXDirection / width
+ }
+}
+
+private fun GraphicsLayerScope.calculatePredictiveBackScaleY(
+ drawerPredictiveBackState: DrawerPredictiveBackState
+): Float {
+ val height = size.height
+ return if (height.isNaN() || height == 0f) {
+ 1f
+ } else {
+ 1f - drawerPredictiveBackState.scaleYDistance / height
+ }
+}
+
+@Composable
+internal expect fun DrawerPredictiveBackHandler(
+ drawerState: DrawerState,
+ content: @Composable (DrawerPredictiveBackState) -> Unit
+)
+
/**
* Object to hold default values for [ModalNavigationDrawer]
*/
@@ -861,6 +1033,36 @@
val ItemPadding = PaddingValues(horizontal = 12.dp)
}
+@Stable
+internal class DrawerPredictiveBackState {
+
+ var swipeEdgeMatchesDrawer by mutableStateOf(true)
+
+ var scaleXDistance by mutableFloatStateOf(0f)
+
+ var scaleYDistance by mutableFloatStateOf(0f)
+
+ fun update(
+ progress: Float,
+ swipeEdgeLeft: Boolean,
+ isRtl: Boolean,
+ maxScaleXDistanceGrow: Float,
+ maxScaleXDistanceShrink: Float,
+ maxScaleYDistance: Float
+ ) {
+ swipeEdgeMatchesDrawer = swipeEdgeLeft != isRtl
+ val maxScaleXDistance =
+ if (swipeEdgeMatchesDrawer) maxScaleXDistanceGrow else maxScaleXDistanceShrink
+ scaleXDistance = lerp(0f, maxScaleXDistance, progress)
+ scaleYDistance = lerp(0f, maxScaleYDistance, progress)
+ }
+ fun clear() {
+ swipeEdgeMatchesDrawer = true
+ scaleXDistance = 0f
+ scaleYDistance = 0f
+ }
+}
+
private class DefaultDrawerItemsColor(
val selectedIconColor: Color,
val unselectedIconColor: Color,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
index 1a41903..b81a1fe 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
@@ -29,6 +29,9 @@
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PageSize
import androidx.compose.foundation.pager.PagerDefaults
@@ -65,18 +68,24 @@
* A horizontal carousel meant to display many items at once for quick browsing of smaller content
* like album art or photo thumbnails.
*
- * Note that this carousel may adjust the size of large items. In order to ensure a mix of large,
+ * Note that this carousel may adjust the size of items in order to ensure a mix of large,
* medium, and small items fit perfectly into the available space and are arranged in a
- * visually pleasing way, this carousel finds the nearest number of large items that
- * will fit the container and adjusts their size to fit, if necessary.
+ * visually pleasing way. Carousel then lays out items using the large item size and clips
+ * (or masks) items depending on their scroll offset to create items which smoothly expand
+ * and collapse between the large, medium, and small sizes.
*
* For more information, see <a href="https://material.io/components/carousel/overview">design
* guidelines</a>.
*
+ * Example of a multi-browse carousel:
+ * @sample androidx.compose.material3.samples.HorizontalMultiBrowseCarouselSample
+ *
* @param state The state object to be used to control the carousel's state
- * @param preferredItemWidth The width the fully visible items would like to be in the main axis.
- * This width is a target and will likely be adjusted by carousel in order to fit a whole number of
- * items within the container
+ * @param preferredItemWidth The width that large, fully visible items would like to be in the
+ * horizontal axis. This width is a target and will likely be adjusted by carousel in order to fit
+ * a whole number of items within the container. Carousel adjusts small items first (between the
+ * [minSmallItemWidth] and [maxSmallItemWidth]) then medium items when present, and finally large
+ * items if necessary.
* @param modifier A modifier instance to be applied to this carousel container
* @param itemSpacing The amount of space used to separate items in the carousel
* @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures
@@ -86,13 +95,14 @@
* @param maxSmallItemWidth The maximum allowable width of small items in dp. Depending on the
* [preferredItemWidth] and the width of the carousel, the small item width will be chosen from a
* range of [minSmallItemWidth] and [maxSmallItemWidth]
+ * @param contentPadding a padding around the whole content. This will add padding for the
+ * content after it has been clipped. You can use it to add a padding before the first item or
+ * after the last one. Use [itemSpacing] to add spacing between the items.
* @param content The carousel's content Composable
- *
- * TODO: Add sample link
*/
@ExperimentalMaterial3Api
@Composable
-internal fun HorizontalMultiBrowseCarousel(
+fun HorizontalMultiBrowseCarousel(
state: CarouselState,
preferredItemWidth: Dp,
modifier: Modifier = Modifier,
@@ -101,6 +111,7 @@
CarouselDefaults.singleAdvanceFlingBehavior(state = state),
minSmallItemWidth: Dp = CarouselDefaults.MinSmallItemSize,
maxSmallItemWidth: Dp = CarouselDefaults.MaxSmallItemSize,
+ contentPadding: PaddingValues = PaddingValues(0.dp),
content: @Composable CarouselScope.(itemIndex: Int) -> Unit
) {
val density = LocalDensity.current
@@ -120,6 +131,7 @@
)
}
},
+ contentPadding = contentPadding,
modifier = modifier,
itemSpacing = itemSpacing,
flingBehavior = flingBehavior,
@@ -140,23 +152,28 @@
* For more information, see <a href="https://material.io/components/carousel/overview">design
* guidelines</a>.
*
+ * Example of an uncontained carousel:
+ * @sample androidx.compose.material3.samples.HorizontalUncontainedCarouselSample
+ *
* @param state The state object to be used to control the carousel's state
* @param itemWidth The width of items in the carousel
* @param modifier A modifier instance to be applied to this carousel container
* @param itemSpacing The amount of space used to separate items in the carousel
* @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures
+ * @param contentPadding a padding around the whole content. This will add padding for the
+ * content after it has been clipped. You can use it to add a padding before the first item or
+ * after the last one. Use [itemSpacing] to add spacing between the items.
* @param content The carousel's content Composable
- *
- * TODO: Add sample link
*/
@ExperimentalMaterial3Api
@Composable
-internal fun HorizontalUncontainedCarousel(
+fun HorizontalUncontainedCarousel(
state: CarouselState,
itemWidth: Dp,
modifier: Modifier = Modifier,
itemSpacing: Dp = 0.dp,
flingBehavior: TargetedFlingBehavior = CarouselDefaults.noSnapFlingBehavior(),
+ contentPadding: PaddingValues = PaddingValues(0.dp),
content: @Composable CarouselScope.(itemIndex: Int) -> Unit
) {
val density = LocalDensity.current
@@ -173,6 +190,7 @@
)
}
},
+ contentPadding = contentPadding,
modifier = modifier,
itemSpacing = itemSpacing,
flingBehavior = flingBehavior,
@@ -190,19 +208,22 @@
* @param orientation The layout orientation of the carousel
* @param keylineList The list of keylines that are fixed positions along the scrolling axis which
* define the state an item should be in when its center is co-located with the keyline's position.
+ * @param contentPadding a padding around the whole content. This will add padding for the
* @param modifier A modifier instance to be applied to this carousel outer layout
+ * content after it has been clipped. You can use it to add a padding before the first item or
+ * after the last one. Use [itemSpacing] to add spacing between the items.
* @param itemSpacing The amount of space used to separate items in the carousel
* @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures
* @param content The carousel's content Composable where each call is passed the index, from the
* total item count, of the item being composed
- * TODO: Add sample link
*/
-@ExperimentalMaterial3Api
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun Carousel(
state: CarouselState,
orientation: Orientation,
keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList?,
+ contentPadding: PaddingValues,
modifier: Modifier = Modifier,
itemSpacing: Dp = 0.dp,
flingBehavior: TargetedFlingBehavior =
@@ -210,7 +231,11 @@
content: @Composable CarouselScope.(itemIndex: Int) -> Unit
) {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
- val pageSize = remember(keylineList) { CarouselPageSize(keylineList) }
+ val beforeContentPadding = contentPadding.calculateBeforeContentPadding(orientation)
+ val afterContentPadding = contentPadding.calculateAfterContentPadding(orientation)
+ val pageSize = remember(keylineList) {
+ CarouselPageSize(keylineList, beforeContentPadding, afterContentPadding)
+ }
val outOfBoundsPageCount = remember(pageSize.strategy.itemMainAxisSize) {
calculateOutOfBounds(pageSize.strategy)
@@ -228,6 +253,11 @@
if (orientation == Orientation.Horizontal) {
HorizontalPager(
state = state.pagerState,
+ // Only pass cross axis padding as main axis padding will be handled by the strategy
+ contentPadding = PaddingValues(
+ top = contentPadding.calculateTopPadding(),
+ bottom = contentPadding.calculateBottomPadding()
+ ),
pageSize = pageSize,
pageSpacing = itemSpacing,
outOfBoundsPageCount = outOfBoundsPageCount,
@@ -250,6 +280,11 @@
} else if (orientation == Orientation.Vertical) {
VerticalPager(
state = state.pagerState,
+ // Only pass cross axis padding as main axis padding will be handled by the strategy
+ contentPadding = PaddingValues(
+ start = contentPadding.calculateStartPadding(LocalLayoutDirection.current),
+ end = contentPadding.calculateEndPadding(LocalLayoutDirection.current)
+ ),
pageSize = pageSize,
pageSpacing = itemSpacing,
outOfBoundsPageCount = outOfBoundsPageCount,
@@ -272,6 +307,28 @@
}
}
+@Composable
+private fun PaddingValues.calculateBeforeContentPadding(orientation: Orientation): Float {
+ val dpValue = if (orientation == Orientation.Vertical) {
+ calculateTopPadding()
+ } else {
+ calculateStartPadding(LocalLayoutDirection.current)
+ }
+
+ return with(LocalDensity.current) { dpValue.toPx() }
+}
+
+@Composable
+private fun PaddingValues.calculateAfterContentPadding(orientation: Orientation): Float {
+ val dpValue = if (orientation == Orientation.Vertical) {
+ calculateBottomPadding()
+ } else {
+ calculateEndPadding(LocalLayoutDirection.current)
+ }
+
+ return with(LocalDensity.current) { dpValue.toPx() }
+}
+
internal fun calculateOutOfBounds(strategy: Strategy): Int {
if (!strategy.isValid()) {
return PagerDefaults.OutOfBoundsPageCount
@@ -297,11 +354,18 @@
* define the state an item should be in when its center is co-located with the keyline's position.
*/
private class CarouselPageSize(
- keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList?
+ keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList?,
+ private val beforeContentPadding: Float,
+ private val afterContentPadding: Float
) : PageSize {
val strategy = Strategy(keylineList)
override fun Density.calculateMainAxisPageSize(availableSpace: Int, pageSpacing: Int): Int {
- strategy.apply(availableSpace.toFloat(), pageSpacing.toFloat())
+ strategy.apply(
+ availableSpace.toFloat(),
+ pageSpacing.toFloat(),
+ beforeContentPadding,
+ afterContentPadding
+ )
return if (strategy.isValid()) {
strategy.itemMainAxisSize.roundToInt()
} else {
@@ -337,7 +401,7 @@
* @param state the carousel state
* @param strategy the strategy used to mask and translate items in the carousel
* @param itemPositionMap the position of each index when it is the current item
- * @param isRtl whether or not the carousel is rtl
+ * @param isRtl true if the layout direction is right-to-left
*/
@OptIn(ExperimentalMaterial3Api::class)
internal fun Modifier.carouselItem(
@@ -503,12 +567,7 @@
* Contains the default values used by [Carousel].
*/
@ExperimentalMaterial3Api
-internal object CarouselDefaults {
- /** The minimum size that a carousel strategy can choose its small items to be. **/
- val MinSmallItemSize = 40.dp
-
- /** The maximum size that a carousel strategy can choose its small items to be. **/
- val MaxSmallItemSize = 56.dp
+object CarouselDefaults {
/**
* A [TargetedFlingBehavior] that limits a fling to one item at a time. [snapAnimationSpec] can
@@ -602,6 +661,12 @@
return rememberSnapFlingBehavior(snapLayoutInfoProvider = decayLayoutInfoProvider)
}
+ /** The minimum size that a carousel strategy can choose its small items to be. **/
+ internal val MinSmallItemSize = 40.dp
+
+ /** The maximum size that a carousel strategy can choose its small items to be. **/
+ internal val MaxSmallItemSize = 56.dp
+
internal val AnchorSize = 10.dp
internal const val MediumLargeItemDiffThreshold = 0.85f
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselScope.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselScope.kt
index 94dbda2..467d8a5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselScope.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselScope.kt
@@ -22,7 +22,7 @@
* Receiver scope for [Carousel].
*/
@ExperimentalMaterial3Api
-internal sealed interface CarouselScope
+sealed interface CarouselScope
@ExperimentalMaterial3Api
internal object CarouselScopeImpl : CarouselScope
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
index b6d989a..1046082 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
@@ -16,7 +16,7 @@
package androidx.compose.material3.carousel
-import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.annotation.FloatRange
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.ScrollableState
@@ -32,14 +32,15 @@
* The state that can be used to control all types of carousels.
*
* @param currentItem the current item to be scrolled to.
- * @param currentItemOffsetFraction the current item offset as a fraction of the item size.
+ * @param currentItemOffsetFraction the offset of the current item as a fraction of the item's size.
+ * This should vary between -0.5 and 0.5 and indicates how to offset the current item from the
+ * snapped position.
* @param itemCount the number of items this Carousel will have.
*/
-@OptIn(ExperimentalFoundationApi::class)
@ExperimentalMaterial3Api
-internal class CarouselState(
+class CarouselState(
currentItem: Int = 0,
- currentItemOffsetFraction: Float = 0F,
+ @FloatRange(from = -0.5, to = 0.5) currentItemOffsetFraction: Float = 0f,
itemCount: () -> Int
) : ScrollableState {
var itemCountState = mutableStateOf(itemCount)
@@ -92,7 +93,7 @@
*/
@ExperimentalMaterial3Api
@Composable
-internal fun rememberCarouselState(
+fun rememberCarouselState(
initialItem: Int = 0,
itemCount: () -> Int,
): CarouselState {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt
index c8a7af0..9caf196 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt
@@ -44,21 +44,21 @@
val endStepsSize = endKeylineSteps.size + numOfFocalKeylines
for (itemIndex in 0 until itemCount) {
- map[itemIndex] = (defaultKeylines.firstFocal.offset -
- defaultKeylines.firstFocal.size / 2F).roundToInt()
+ map[itemIndex] = (defaultKeylines.firstFocal.unadjustedOffset -
+ strategy.itemMainAxisSize / 2F).roundToInt()
if (itemIndex < startStepsSize) {
var startIndex = max(0, startStepsSize - 1 - itemIndex)
startIndex = min(startKeylineSteps.size - 1, startIndex)
val startKeylines = startKeylineSteps[startIndex]
- map[itemIndex] = (startKeylines.firstFocal.offset -
- startKeylines.firstFocal.size / 2f).roundToInt()
+ map[itemIndex] = (startKeylines.firstFocal.unadjustedOffset -
+ strategy.itemMainAxisSize / 2f).roundToInt()
}
if (itemCount > numOfFocalKeylines + 1 && itemIndex >= itemCount - endStepsSize) {
var endIndex = max(0, itemIndex - itemCount + endStepsSize)
endIndex = min(endKeylineSteps.size - 1, endIndex)
val endKeylines = endKeylineSteps[endIndex]
- map[itemIndex] = (endKeylines.firstFocal.offset -
- endKeylines.firstFocal.size / 2f).roundToInt()
+ map[itemIndex] = (endKeylines.firstFocal.unadjustedOffset -
+ strategy.itemMainAxisSize / 2f).roundToInt()
}
}
return map
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
index 9c654b2..6b47ff5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
@@ -65,10 +65,7 @@
// of the large item and medium items are sized between large and small items. Clamp the
// small target size within our min-max range and as close to 1/3 of the target large item
// size as possible.
- val targetSmallSize: Float = (targetLargeSize / 3f + itemSpacing).coerceIn(
- minSmallItemSize + itemSpacing,
- maxSmallItemSize + itemSpacing
- )
+ val targetSmallSize: Float = (targetLargeSize / 3f).coerceIn(minSmallItemSize, maxSmallItemSize)
val targetMediumSize = (targetLargeSize + targetSmallSize) / 2f
if (carouselMainAxisSize < minSmallItemSize * 2) {
@@ -108,15 +105,15 @@
var mediumCount = arrangement.mediumCount
while (keylineSurplus > 0) {
if (smallCount > 0) {
- smallCount -= 1;
+ smallCount -= 1
} else if (mediumCount > 1) {
// Keep at least 1 medium so the large items don't fill the entire carousel in new
// strategy.
- mediumCount -= 1;
+ mediumCount -= 1
}
// large items don't need to be removed even if they are a surplus because large items
// are already fully unmasked.
- keylineSurplus -= 1;
+ keylineSurplus -= 1
}
arrangement = Arrangement.findLowestCostArrangement(
availableSpace = carouselMainAxisSize,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
index 5641be1..916efe0 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
@@ -22,8 +22,11 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.setValue
+import androidx.compose.ui.util.fastFilter
import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastMapIndexed
import androidx.compose.ui.util.lerp
+import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
@@ -84,6 +87,8 @@
internal var availableSpace: Float = 0f
/** The spacing between each item. */
internal var itemSpacing: Float = 0f
+ internal var beforeContentPadding: Float = 0f
+ internal var afterContentPadding: Float = 0f
/** The size of items when in focus and fully unmasked. */
internal var itemMainAxisSize by mutableFloatStateOf(0f)
@@ -101,8 +106,15 @@
* This method must be called before a strategy can be used by carousel.
*
* @param availableSpace the size of the carousel container in scrolling axis
+ * @param beforeContentPadding the padding to add before the list content
+ * @param afterContentPadding the padding to add after the list content
*/
- internal fun apply(availableSpace: Float, itemSpacing: Float): Strategy {
+ internal fun apply(
+ availableSpace: Float,
+ itemSpacing: Float,
+ beforeContentPadding: Float,
+ afterContentPadding: Float
+ ): Strategy {
// Skip computing new keylines and updating this strategy if
// available space has not changed.
if (this.availableSpace == availableSpace && this.itemSpacing == itemSpacing) {
@@ -110,15 +122,16 @@
}
val keylineList = keylineList.invoke(availableSpace, itemSpacing) ?: return this
- val startKeylineSteps = getStartKeylineSteps(keylineList, availableSpace, itemSpacing)
+ val startKeylineSteps =
+ getStartKeylineSteps(keylineList, availableSpace, itemSpacing, beforeContentPadding)
val endKeylineSteps =
- getEndKeylineSteps(keylineList, availableSpace, itemSpacing)
+ getEndKeylineSteps(keylineList, availableSpace, itemSpacing, afterContentPadding)
// TODO: Update this to use the first/last focal keylines to calculate shift?
- val startShiftDistance = startKeylineSteps.last().first().unadjustedOffset -
- keylineList.first().unadjustedOffset
- val endShiftDistance = keylineList.last().unadjustedOffset -
- endKeylineSteps.last().last().unadjustedOffset
+ val startShiftDistance = max(startKeylineSteps.last().first().unadjustedOffset -
+ keylineList.first().unadjustedOffset, beforeContentPadding)
+ val endShiftDistance = max(keylineList.last().unadjustedOffset -
+ endKeylineSteps.last().last().unadjustedOffset, afterContentPadding)
this.defaultKeylines = keylineList
this.defaultKeylines = keylineList
@@ -138,6 +151,8 @@
)
this.availableSpace = availableSpace
this.itemSpacing = itemSpacing
+ this.beforeContentPadding = beforeContentPadding
+ this.afterContentPadding = afterContentPadding
this.itemMainAxisSize = defaultKeylines.firstFocal.size
return this
@@ -229,6 +244,8 @@
if (isValid() != other.isValid()) return false
if (availableSpace != other.availableSpace) return false
if (itemSpacing != other.itemSpacing) return false
+ if (beforeContentPadding != other.beforeContentPadding) return false
+ if (afterContentPadding != other.afterContentPadding) return false
if (itemMainAxisSize != other.itemMainAxisSize) return false
if (startShiftDistance != other.startShiftDistance) return false
if (endShiftDistance != other.endShiftDistance) return false
@@ -247,6 +264,8 @@
var result = isValid().hashCode()
result = 31 * result + availableSpace.hashCode()
result = 31 * result + itemSpacing.hashCode()
+ result = 31 * result + beforeContentPadding.hashCode()
+ result = 31 * result + afterContentPadding.hashCode()
result = 31 * result + itemMainAxisSize.hashCode()
result = 31 * result + startShiftDistance.hashCode()
result = 31 * result + endShiftDistance.hashCode()
@@ -277,12 +296,23 @@
private fun getStartKeylineSteps(
defaultKeylines: KeylineList,
carouselMainAxisSize: Float,
- itemSpacing: Float
+ itemSpacing: Float,
+ beforeContentPadding: Float
): List<KeylineList> {
val steps: MutableList<KeylineList> = mutableListOf()
steps.add(defaultKeylines)
if (defaultKeylines.isFirstFocalItemAtStartOfContainer()) {
+ if (beforeContentPadding != 0f) {
+ steps.add(
+ createShiftedKeylineListForContentPadding(
+ defaultKeylines,
+ carouselMainAxisSize,
+ itemSpacing,
+ beforeContentPadding
+ )
+ )
+ }
return steps
}
@@ -329,6 +359,15 @@
i++
}
+ if (beforeContentPadding != 0f) {
+ steps[steps.lastIndex] = createShiftedKeylineListForContentPadding(
+ steps.last(),
+ carouselMainAxisSize,
+ itemSpacing,
+ beforeContentPadding
+ )
+ }
+
return steps
}
@@ -351,12 +390,21 @@
private fun getEndKeylineSteps(
defaultKeylines: KeylineList,
carouselMainAxisSize: Float,
- itemSpacing: Float
+ itemSpacing: Float,
+ afterContentPadding: Float
): List<KeylineList> {
val steps: MutableList<KeylineList> = mutableListOf()
steps.add(defaultKeylines)
if (defaultKeylines.isLastFocalItemAtEndOfContainer(carouselMainAxisSize)) {
+ if (afterContentPadding != 0f) {
+ steps.add(createShiftedKeylineListForContentPadding(
+ defaultKeylines,
+ carouselMainAxisSize,
+ itemSpacing,
+ -afterContentPadding
+ ))
+ }
return steps
}
@@ -403,10 +451,54 @@
i++
}
+ if (afterContentPadding != 0f) {
+ steps[steps.lastIndex] = createShiftedKeylineListForContentPadding(
+ steps.last(),
+ carouselMainAxisSize,
+ itemSpacing,
+ -afterContentPadding
+ )
+ }
+
return steps
}
/**
+ * Returns a new [KeylineList] identical to [from] but with each keyline's offset shifted
+ * by [contentPadding].
+ */
+ private fun createShiftedKeylineListForContentPadding(
+ from: KeylineList,
+ carouselMainAxisSize: Float,
+ itemSpacing: Float,
+ contentPadding: Float
+ ): KeylineList {
+ val numberOfNonAnchorKeylines = from.fastFilter { !it.isAnchor }.count()
+ val sizeReduction = contentPadding / numberOfNonAnchorKeylines
+ // Let keylineListOf create a new keyline list with offsets adjusted for each item's
+ // reduction in size
+ val newKeylines = keylineListOf(
+ carouselMainAxisSize = carouselMainAxisSize,
+ itemSpacing = itemSpacing,
+ pivotIndex = from.pivotIndex,
+ pivotOffset = from.pivot.offset + contentPadding - (sizeReduction / 2f)
+ ) {
+ from.fastForEach { k -> add(k.size - abs(sizeReduction), k.isAnchor) }
+ }
+
+ // Then reset each item's unadjusted offset back to their original value from the
+ // incoming keyline list. This is necessary because Pager will still be laying out items
+ // end-to-end with the original page size and not the new reduced size.
+ return KeylineList(
+ newKeylines.fastMapIndexed { i, k ->
+ k.copy(
+ unadjustedOffset = from[i].unadjustedOffset
+ )
+ }
+ )
+ }
+
+ /**
* Returns a new [KeylineList] where the keyline at [srcIndex] is moved to [dstIndex] and
* with updated pivot and offsets that reflect any change in focal shift.
*/
diff --git a/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/NavigationDrawer.desktop.kt b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/NavigationDrawer.desktop.kt
new file mode 100644
index 0000000..94baaa8
--- /dev/null
+++ b/compose/material3/material3/src/desktopMain/kotlin/androidx/compose/material3/NavigationDrawer.desktop.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * 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 androidx.compose.material3
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+
+/**
+ * A predictive back handler that does nothing when running in a desktop context, since predictive
+ * back is only supported on Android.
+ *
+ * @param drawerState state of the drawer
+ * @param content content of the rest of the UI
+ */
+@Composable
+internal actual fun DrawerPredictiveBackHandler(
+ drawerState: DrawerState,
+ content: @Composable (DrawerPredictiveBackState) -> Unit
+) {
+ content(remember { DrawerPredictiveBackState() })
+}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
index 02f6601..d2c2079 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
@@ -28,5 +28,5 @@
* IMPORTANT: Whenever updating this value, please make sure to also update `versionTable` and
* `minimumRuntimeVersionInt` in `VersionChecker.kt` of the compiler.
*/
- const val version: Int = 12400
+ const val version: Int = 12500
}
diff --git a/compose/ui/ui/proguard-rules.pro b/compose/ui/ui/proguard-rules.pro
index 1dbcbf7..7aa50f4 100644
--- a/compose/ui/ui/proguard-rules.pro
+++ b/compose/ui/ui/proguard-rules.pro
@@ -17,6 +17,7 @@
# R8 to complain about them not being there during optimization.
-dontwarn android.view.RenderNode
-dontwarn android.view.DisplayListCanvas
+-dontwarn android.view.HardwareCanvas
-keepclassmembers class androidx.compose.ui.platform.ViewLayerContainer {
protected void dispatchGetDisplayList();
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
index 94119f3..b20ed840 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
@@ -58,6 +58,7 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.OpenComposeView
+import androidx.compose.ui.background
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.scale
@@ -65,6 +66,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.gesture.PointerCoords
import androidx.compose.ui.gesture.PointerProperties
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.LayoutCoordinates
@@ -2267,6 +2269,1050 @@
}
/*
+ * Tests TOUCH events are triggered correctly when dynamically adding a NON-pointer input
+ * modifier above an existing pointer input modifier.
+ *
+ * Note: The lambda for the existing pointer input modifier is not re-executed after the
+ * dynamic one is added.
+ *
+ * Specific events:
+ * 1. UI Element (modifier 1 only): PRESS (touch)
+ * 2. UI Element (modifier 1 only): MOVE (touch)
+ * 3. UI Element (modifier 1 only): RELEASE (touch)
+ * 4. Dynamically adds NON-pointer input modifier (between input event streams)
+ * 5. UI Element (modifier 1 and 2): PRESS (touch)
+ * 6. UI Element (modifier 1 and 2): MOVE (touch)
+ * 7. UI Element (modifier 1 and 2): RELEASE (touch)
+ */
+ @Test
+ fun dynamicNonInputModifier_addsAboveExistingModifier_shouldTriggerInNewModifier() {
+ // --> Arrange
+ val originalPointerInputModifierKey = "ORIGINAL_POINTER_INPUT_MODIFIER_KEY_123"
+ var box1LayoutCoordinates: LayoutCoordinates? = null
+
+ val setUpFinishedLatch = CountDownLatch(1)
+
+ var enableDynamicPointerInput by mutableStateOf(false)
+
+ // Events for the lower modifier Box 1
+ var originalPointerInputScopeExecutionCount by mutableStateOf(0)
+ var preexistingModifierPress by mutableStateOf(0)
+ var preexistingModifierMove by mutableStateOf(0)
+ var preexistingModifierRelease by mutableStateOf(0)
+
+ // All other events that should never be triggered in this test
+ var eventsThatShouldNotTrigger by mutableStateOf(false)
+
+ var pointerEvent: PointerEvent? by mutableStateOf(null)
+
+ // Events for the dynamic upper modifier Box 1
+ var dynamicModifierExecuted by mutableStateOf(false)
+
+ // Non-Pointer Input Modifier that is toggled on/off based on passed value.
+ fun Modifier.dynamicallyToggledModifier(enable: Boolean) = if (enable) {
+ dynamicModifierExecuted = true
+ background(Color.Green)
+ } else this
+
+ // Setup UI
+ rule.runOnUiThread {
+ container.setContent {
+ Box(
+ Modifier
+ .size(200.dp)
+ .onGloballyPositioned {
+ box1LayoutCoordinates = it
+ setUpFinishedLatch.countDown()
+ }
+ .dynamicallyToggledModifier(enableDynamicPointerInput)
+ .pointerInput(originalPointerInputModifierKey) {
+ ++originalPointerInputScopeExecutionCount
+ // Reset pointer events when lambda is ran the first time
+ preexistingModifierPress = 0
+ preexistingModifierMove = 0
+ preexistingModifierRelease = 0
+
+ awaitPointerEventScope {
+ while (true) {
+ pointerEvent = awaitPointerEvent()
+ when (pointerEvent!!.type) {
+ PointerEventType.Press -> {
+ ++preexistingModifierPress
+ }
+ PointerEventType.Move -> {
+ ++preexistingModifierMove
+ }
+ PointerEventType.Release -> {
+ ++preexistingModifierRelease
+ }
+ else -> {
+ eventsThatShouldNotTrigger = true
+ }
+ }
+ }
+ }
+ }
+ ) { }
+ }
+ }
+ // Ensure Arrange (setup) step is finished
+ assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
+
+ // --> Act + Assert (interwoven)
+ // DOWN (original modifier only)
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicModifierExecuted).isEqualTo(false)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(0)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original modifier only)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicModifierExecuted).isEqualTo(false)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original modifier only)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicModifierExecuted).isEqualTo(false)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+ enableDynamicPointerInput = true
+ rule.waitForFutureFrame(2)
+
+ // DOWN (original + dynamically added modifiers)
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+
+ rule.runOnUiThread {
+ // There are no pointer input modifiers added above this pointer modifier, so the
+ // same one is used.
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ // The dynamic one has been added, so we execute its thing as well.
+ assertThat(dynamicModifierExecuted).isEqualTo(true)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(2)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicModifierExecuted).isEqualTo(true)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(2)
+ assertThat(preexistingModifierMove).isEqualTo(2)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicModifierExecuted).isEqualTo(true)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(2)
+ assertThat(preexistingModifierMove).isEqualTo(2)
+ assertThat(preexistingModifierRelease).isEqualTo(2)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+ }
+
+ /*
+ * Tests TOUCH events are triggered correctly when dynamically adding a pointer input modifier
+ * ABOVE an existing pointer input modifier.
+ *
+ * Note: The lambda for the existing pointer input modifier **IS** re-executed after the
+ * dynamic pointer input modifier is added above it.
+ *
+ * Specific events:
+ * 1. UI Element (modifier 1 only): PRESS (touch)
+ * 2. UI Element (modifier 1 only): MOVE (touch)
+ * 3. UI Element (modifier 1 only): RELEASE (touch)
+ * 4. Dynamically add pointer input modifier above existing one (between input event streams)
+ * 5. UI Element (modifier 1 and 2): PRESS (touch)
+ * 6. UI Element (modifier 1 and 2): MOVE (touch)
+ * 7. UI Element (modifier 1 and 2): RELEASE (touch)
+ */
+ @Test
+ fun dynamicInputModifierWithKey_addsAboveExistingModifier_shouldTriggerInNewModifier() {
+ // --> Arrange
+ val originalPointerInputModifierKey = "ORIGINAL_POINTER_INPUT_MODIFIER_KEY_123"
+ var box1LayoutCoordinates: LayoutCoordinates? = null
+
+ val setUpFinishedLatch = CountDownLatch(1)
+
+ var enableDynamicPointerInput by mutableStateOf(false)
+
+ // Events for the lower modifier Box 1
+ var originalPointerInputScopeExecutionCount by mutableStateOf(0)
+ var preexistingModifierPress by mutableStateOf(0)
+ var preexistingModifierMove by mutableStateOf(0)
+ var preexistingModifierRelease by mutableStateOf(0)
+
+ // Events for the dynamic upper modifier Box 1
+ var dynamicPointerInputScopeExecutionCount by mutableStateOf(0)
+ var dynamicModifierPress by mutableStateOf(0)
+ var dynamicModifierMove by mutableStateOf(0)
+ var dynamicModifierRelease by mutableStateOf(0)
+
+ // All other events that should never be triggered in this test
+ var eventsThatShouldNotTrigger by mutableStateOf(false)
+
+ var pointerEvent: PointerEvent? by mutableStateOf(null)
+
+ // Pointer Input Modifier that is toggled on/off based on passed value.
+ fun Modifier.dynamicallyToggledPointerInput(
+ enable: Boolean,
+ pointerEventLambda: (pointerEvent: PointerEvent) -> Unit
+ ) = if (enable) {
+ pointerInput(pointerEventLambda) {
+ ++dynamicPointerInputScopeExecutionCount
+
+ // Reset pointer events when lambda is ran the first time
+ dynamicModifierPress = 0
+ dynamicModifierMove = 0
+ dynamicModifierRelease = 0
+
+ awaitPointerEventScope {
+ while (true) {
+ pointerEventLambda(awaitPointerEvent())
+ }
+ }
+ }
+ } else this
+
+ // Setup UI
+ rule.runOnUiThread {
+ container.setContent {
+ Box(
+ Modifier
+ .size(200.dp)
+ .onGloballyPositioned {
+ box1LayoutCoordinates = it
+ setUpFinishedLatch.countDown()
+ }
+ .dynamicallyToggledPointerInput(enableDynamicPointerInput) {
+ when (it.type) {
+ PointerEventType.Press -> {
+ ++dynamicModifierPress
+ }
+ PointerEventType.Move -> {
+ ++dynamicModifierMove
+ }
+ PointerEventType.Release -> {
+ ++dynamicModifierRelease
+ }
+ else -> {
+ eventsThatShouldNotTrigger = true
+ }
+ }
+ }
+ .pointerInput(originalPointerInputModifierKey) {
+ ++originalPointerInputScopeExecutionCount
+ // Reset pointer events when lambda is ran the first time
+ preexistingModifierPress = 0
+ preexistingModifierMove = 0
+ preexistingModifierRelease = 0
+
+ awaitPointerEventScope {
+ while (true) {
+ pointerEvent = awaitPointerEvent()
+ when (pointerEvent!!.type) {
+ PointerEventType.Press -> {
+ ++preexistingModifierPress
+ }
+ PointerEventType.Move -> {
+ ++preexistingModifierMove
+ }
+ PointerEventType.Release -> {
+ ++preexistingModifierRelease
+ }
+ else -> {
+ eventsThatShouldNotTrigger = true
+ }
+ }
+ }
+ }
+ }
+ ) { }
+ }
+ }
+ // Ensure Arrange (setup) step is finished
+ assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
+
+ // --> Act + Assert (interwoven)
+ // DOWN (original modifier only)
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(0)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original modifier only)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original modifier only)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ enableDynamicPointerInput = true
+ rule.waitForFutureFrame(2)
+
+ // Important Note: Even though we reset all the pointer input blocks, the initial lambda is
+ // lazily executed, meaning it won't reset the values until the first event comes in, so
+ // the previously set values are still the same until an event comes in.
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+ }
+
+ // DOWN (original + dynamically added modifiers)
+ // Now an event comes in, so the lambdas are both executed completely (dynamic one for the
+ // first time and the existing one for a second time [since it was moved]).
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+
+ rule.runOnUiThread {
+ // While the original pointer input block is being reused after a new one is added, it
+ // is reset (since things have changed with the Modifiers), so the entire block is
+ // executed again to allow devs to reset their gesture detectors for the new Modifier
+ // chain changes.
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
+ // The dynamic one has been added, so we execute its thing as well.
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(0)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(1)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(1)
+ assertThat(dynamicModifierMove).isEqualTo(1)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(1)
+ assertThat(dynamicModifierMove).isEqualTo(1)
+ assertThat(dynamicModifierRelease).isEqualTo(1)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+ }
+
+ /*
+ * Tests TOUCH events are triggered incorrectly when dynamically adding a pointer input modifier
+ * (which uses Unit for its key [bad]) ABOVE an existing pointer input modifier. This is more
+ * of an "education test" for developers to see how things can go wrong if you use "Unit" for
+ * your key in pointer input and pointer input modifiers are later added dynamically.
+ *
+ * Note: Even though we are dynamically adding a new pointer input modifier above the existing
+ * pointer input modifier, Compose actually reuses the existing pointer input modifier to
+ * contain the new pointer input modifier. It then adds a new pointer input modifier below that
+ * one and copies in the original (non-dynamic) pointer input modifier into that. However, in
+ * this case, because we are using the "Unit" for both keys, Compose thinks they are the same
+ * pointer input modifier, so it never replaces the existing lambda with the dynamic pointer
+ * input modifier node's lambda. This is why you should not use Unit for your key.
+ *
+ * Why can't the lambdas passed into pointer input be compared? We can't memoize them because
+ * they are outside of a Compose scope (defined in a Modifier extension function), so
+ * developers need to pass a unique key(s) as a way to let us know when to update the lambda.
+ * You can do that with a unique key for each pointer input modifier and/or take it a step
+ * further and use captured values in the lambda as keys (ones that change lambda
+ * behavior).
+ *
+ * Specific events:
+ * 1. UI Element (modifier 1 only): PRESS (touch)
+ * 2. UI Element (modifier 1 only): MOVE (touch)
+ * 3. UI Element (modifier 1 only): RELEASE (touch)
+ * 4. Dynamically add pointer input modifier above existing one (between input event streams)
+ * 5. UI Element (modifier 1 and 2): PRESS (touch)
+ * 6. UI Element (modifier 1 and 2): MOVE (touch)
+ * 7. UI Element (modifier 1 and 2): RELEASE (touch)
+ */
+ @Test
+ fun dynamicInputModifierWithUnitKey_addsAboveExistingModifier_failsToTriggerNewModifier() {
+ // --> Arrange
+ var box1LayoutCoordinates: LayoutCoordinates? = null
+
+ val setUpFinishedLatch = CountDownLatch(1)
+
+ var enableDynamicPointerInput by mutableStateOf(false)
+
+ // Events for the lower modifier Box 1
+ var originalPointerInputScopeExecutionCount by mutableStateOf(0)
+ var preexistingModifierPress by mutableStateOf(0)
+ var preexistingModifierMove by mutableStateOf(0)
+ var preexistingModifierRelease by mutableStateOf(0)
+
+ // Events for the dynamic upper modifier Box 1
+ var dynamicPointerInputScopeExecutionCount by mutableStateOf(0)
+ var dynamicModifierPress by mutableStateOf(0)
+ var dynamicModifierMove by mutableStateOf(0)
+ var dynamicModifierRelease by mutableStateOf(0)
+
+ // All other events that should never be triggered in this test
+ var eventsThatShouldNotTrigger by mutableStateOf(false)
+
+ var pointerEvent: PointerEvent? by mutableStateOf(null)
+
+ // Pointer Input Modifier that is toggled on/off based on passed value.
+ fun Modifier.dynamicallyToggledPointerInput(
+ enable: Boolean,
+ pointerEventLambda: (pointerEvent: PointerEvent) -> Unit
+ ) = if (enable) {
+ pointerInput(Unit) {
+ ++dynamicPointerInputScopeExecutionCount
+
+ // Reset pointer events when lambda is ran the first time
+ dynamicModifierPress = 0
+ dynamicModifierMove = 0
+ dynamicModifierRelease = 0
+
+ awaitPointerEventScope {
+ while (true) {
+ pointerEventLambda(awaitPointerEvent())
+ }
+ }
+ }
+ } else this
+
+ // Setup UI
+ rule.runOnUiThread {
+ container.setContent {
+ Box(
+ Modifier
+ .size(200.dp)
+ .onGloballyPositioned {
+ box1LayoutCoordinates = it
+ setUpFinishedLatch.countDown()
+ }
+ .dynamicallyToggledPointerInput(enableDynamicPointerInput) {
+ when (it.type) {
+ PointerEventType.Press -> {
+ ++dynamicModifierPress
+ }
+ PointerEventType.Move -> {
+ ++dynamicModifierMove
+ }
+ PointerEventType.Release -> {
+ ++dynamicModifierRelease
+ }
+ else -> {
+ eventsThatShouldNotTrigger = true
+ }
+ }
+ }
+ .pointerInput(Unit) {
+ ++originalPointerInputScopeExecutionCount
+ awaitPointerEventScope {
+ while (true) {
+ pointerEvent = awaitPointerEvent()
+ when (pointerEvent!!.type) {
+ PointerEventType.Press -> {
+ ++preexistingModifierPress
+ }
+ PointerEventType.Move -> {
+ ++preexistingModifierMove
+ }
+ PointerEventType.Release -> {
+ ++preexistingModifierRelease
+ }
+ else -> {
+ eventsThatShouldNotTrigger = true
+ }
+ }
+ }
+ }
+ }
+ ) { }
+ }
+ }
+ // Ensure Arrange (setup) step is finished
+ assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
+
+ // --> Act + Assert (interwoven)
+ // DOWN (original modifier only)
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(0)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original modifier only)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original modifier only)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ enableDynamicPointerInput = true
+ rule.waitForFutureFrame(2)
+
+ // Important Note: I'm not resetting the variable counters in this test.
+
+ // DOWN (original + dynamically added modifiers)
+ // Now an event comes in, so the lambdas are both executed completely for the first time.
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+
+ rule.runOnUiThread {
+ // While the original pointer input block is being reused after a new one is added, it
+ // is reset (since things have changed with the Modifiers), so the entire block is
+ // executed again to allow devs to reset their gesture detectors for the new Modifier
+ // chain changes.
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
+ // The dynamic one has been added, so we execute its thing as well.
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ // This is 2 because the dynamic modifier added before the existing one, is using Unit
+ // for the key, so the comparison shows that it doesn't need to update the lambda...
+ // Thus, it uses the old lambda (why it is very important you don't use Unit for your
+ // key.
+ assertThat(preexistingModifierPress).isEqualTo(3)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(3)
+ assertThat(preexistingModifierMove).isEqualTo(3)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(2)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(3)
+ assertThat(preexistingModifierMove).isEqualTo(3)
+ assertThat(preexistingModifierRelease).isEqualTo(3)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+ }
+
+ /*
+ * Tests TOUCH events are triggered correctly when dynamically adding a pointer input
+ * modifier BELOW an existing pointer input modifier.
+ *
+ * Note: The lambda for the existing pointer input modifier is NOT re-executed after the
+ * dynamic one is added below it (since it doesn't impact it).
+ *
+ * Specific events:
+ * 1. UI Element (modifier 1 only): PRESS (touch)
+ * 2. UI Element (modifier 1 only): MOVE (touch)
+ * 3. UI Element (modifier 1 only): RELEASE (touch)
+ * 4. Dynamically add pointer input modifier below existing one (between input event streams)
+ * 5. UI Element (modifier 1 and 2): PRESS (touch)
+ * 6. UI Element (modifier 1 and 2): MOVE (touch)
+ * 7. UI Element (modifier 1 and 2): RELEASE (touch)
+ */
+ @Test
+ fun dynamicInputModifierWithKey_addsBelowExistingModifier_shouldTriggerInNewModifier() {
+ // --> Arrange
+ val originalPointerInputModifierKey = "ORIGINAL_POINTER_INPUT_MODIFIER_KEY_123"
+ var box1LayoutCoordinates: LayoutCoordinates? = null
+
+ val setUpFinishedLatch = CountDownLatch(1)
+
+ var enableDynamicPointerInput by mutableStateOf(false)
+
+ // Events for the lower modifier Box 1
+ var originalPointerInputScopeExecutionCount by mutableStateOf(0)
+ var preexistingModifierPress by mutableStateOf(0)
+ var preexistingModifierMove by mutableStateOf(0)
+ var preexistingModifierRelease by mutableStateOf(0)
+
+ // Events for the dynamic upper modifier Box 1
+ var dynamicPointerInputScopeExecutionCount by mutableStateOf(0)
+ var dynamicModifierPress by mutableStateOf(0)
+ var dynamicModifierMove by mutableStateOf(0)
+ var dynamicModifierRelease by mutableStateOf(0)
+
+ // All other events that should never be triggered in this test
+ var eventsThatShouldNotTrigger by mutableStateOf(false)
+
+ var pointerEvent: PointerEvent? by mutableStateOf(null)
+
+ // Pointer Input Modifier that is toggled on/off based on passed value.
+ fun Modifier.dynamicallyToggledPointerInput(
+ enable: Boolean,
+ pointerEventLambda: (pointerEvent: PointerEvent) -> Unit
+ ) = if (enable) {
+ pointerInput(pointerEventLambda) {
+ ++dynamicPointerInputScopeExecutionCount
+
+ // Reset pointer events when lambda is ran the first time
+ dynamicModifierPress = 0
+ dynamicModifierMove = 0
+ dynamicModifierRelease = 0
+
+ awaitPointerEventScope {
+ while (true) {
+ pointerEventLambda(awaitPointerEvent())
+ }
+ }
+ }
+ } else this
+
+ // Setup UI
+ rule.runOnUiThread {
+ container.setContent {
+ Box(
+ Modifier
+ .size(200.dp)
+ .onGloballyPositioned {
+ box1LayoutCoordinates = it
+ setUpFinishedLatch.countDown()
+ }
+ .pointerInput(originalPointerInputModifierKey) {
+ ++originalPointerInputScopeExecutionCount
+ // Reset pointer events when lambda is ran the first time
+ preexistingModifierPress = 0
+ preexistingModifierMove = 0
+ preexistingModifierRelease = 0
+
+ awaitPointerEventScope {
+ while (true) {
+ pointerEvent = awaitPointerEvent()
+ when (pointerEvent!!.type) {
+ PointerEventType.Press -> {
+ ++preexistingModifierPress
+ }
+ PointerEventType.Move -> {
+ ++preexistingModifierMove
+ }
+ PointerEventType.Release -> {
+ ++preexistingModifierRelease
+ }
+ else -> {
+ eventsThatShouldNotTrigger = true
+ }
+ }
+ }
+ }
+ }
+ .dynamicallyToggledPointerInput(enableDynamicPointerInput) {
+ when (it.type) {
+ PointerEventType.Press -> {
+ ++dynamicModifierPress
+ }
+ PointerEventType.Move -> {
+ ++dynamicModifierMove
+ }
+ PointerEventType.Release -> {
+ ++dynamicModifierRelease
+ }
+ else -> {
+ eventsThatShouldNotTrigger = true
+ }
+ }
+ }
+ ) { }
+ }
+ }
+ // Ensure Arrange (setup) step is finished
+ assertTrue(setUpFinishedLatch.await(2, TimeUnit.SECONDS))
+
+ // --> Act + Assert (interwoven)
+ // DOWN (original modifier only)
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(0)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original modifier only)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(0)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original modifier only)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(0)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(1)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(0)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ enableDynamicPointerInput = true
+ rule.waitForFutureFrame(2)
+
+ // DOWN (original + dynamically added modifiers)
+ dispatchTouchEvent(ACTION_DOWN, box1LayoutCoordinates!!)
+
+ rule.runOnUiThread {
+ // Because the new pointer input modifier is added below the existing one, the existing
+ // one doesn't change.
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(2)
+ assertThat(preexistingModifierMove).isEqualTo(1)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(1)
+ assertThat(dynamicModifierMove).isEqualTo(0)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // MOVE (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_MOVE,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(2)
+ assertThat(preexistingModifierMove).isEqualTo(2)
+ assertThat(preexistingModifierRelease).isEqualTo(1)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(1)
+ assertThat(dynamicModifierMove).isEqualTo(1)
+ assertThat(dynamicModifierRelease).isEqualTo(0)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+
+ // UP (original + dynamically added modifiers)
+ dispatchTouchEvent(
+ ACTION_UP,
+ box1LayoutCoordinates!!,
+ Offset(0f, box1LayoutCoordinates!!.size.height / 2 - 1f)
+ )
+ rule.runOnUiThread {
+ assertThat(originalPointerInputScopeExecutionCount).isEqualTo(1)
+ assertThat(dynamicPointerInputScopeExecutionCount).isEqualTo(1)
+
+ // Verify Box 1 existing modifier events
+ assertThat(preexistingModifierPress).isEqualTo(2)
+ assertThat(preexistingModifierMove).isEqualTo(2)
+ assertThat(preexistingModifierRelease).isEqualTo(2)
+
+ // Verify Box 1 dynamically added modifier events
+ assertThat(dynamicModifierPress).isEqualTo(1)
+ assertThat(dynamicModifierMove).isEqualTo(1)
+ assertThat(dynamicModifierRelease).isEqualTo(1)
+
+ assertThat(pointerEvent).isNotNull()
+ assertThat(eventsThatShouldNotTrigger).isFalse()
+ }
+ }
+
+ /*
* Tests a full mouse event cycle from a press and release.
*
* Important Note: The pointer id should stay the same throughout all these events (part of the
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index ff9ed9d..3dc311e 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -278,7 +278,6 @@
<trusted-key id="835A685C8C6F49C54980E5CAF406F31BC1468EBA" group="org.jcodec"/>
<trusted-key id="842AFB86375D805422835BFD82B5574242C20D6F" group="org.antlr"/>
<trusted-key id="8461EFA0E74ABAE010DE66994EB27DB2A3B88B8B" group="^androidx\..*" regex="true"/>
- <trusted-key id="A5F483CD733A4EBAEA378B2AE88979FB9B30ACF2" group="^androidx\..*" regex="true"/>
<trusted-key id="84789D24DF77A32433CE1F079EB80E92EB2135B1">
<trusting group="org.apache"/>
<trusting group="org.apache.maven" name="maven-parent"/>
@@ -340,6 +339,7 @@
<trusted-key id="A4FD709CC4B0515F2E6AF04E218FA0F6A941A037" group="com.github.kevinstern"/>
<trusted-key id="A5B2DDE7843E7CA3E8CAABD02383163BC40844FD" group="org.reactivestreams"/>
<trusted-key id="A5BD02B93E7A40482EB1D66A5F69AD087600B22C" group="org.ow2.asm"/>
+ <trusted-key id="A5F483CD733A4EBAEA378B2AE88979FB9B30ACF2" group="^androidx\..*" regex="true"/>
<trusted-key id="A6D6C97108B8585F91B158748671A8DF71296252" group="^com[.]squareup($|([.].*))" regex="true"/>
<trusted-key id="A7892505CF1A58076453E52D7999BEFBA1039E8B" group="net.bytebuddy"/>
<trusted-key id="AA417737BD805456DB3CBDDE6601E5C08DCCBB96" group="info.picocli" name="picocli"/>
diff --git a/gradlew b/gradlew
index 28d0634..04379b2 100755
--- a/gradlew
+++ b/gradlew
@@ -243,11 +243,6 @@
disableCi=false
fi
-# workaround for https://github.com/gradle/gradle/issues/18386
-if [[ " ${@} " =~ " --profile " ]]; then
- mkdir -p reports
-fi
-
# Expand some arguments
for compact in "--ci" "--strict" "--clean" "--no-ci"; do
expanded=""
@@ -259,7 +254,8 @@
-Pandroidx.enableAffectedModuleDetection\
-Pandroidx.printTimestamps\
--no-watch-fs\
- -Pandroidx.highMemory"
+ -Pandroidx.highMemory\
+ --profile"
fi
fi
if [ "$compact" == "--strict" ]; then
@@ -302,6 +298,11 @@
fi
done
+# workaround for https://github.com/gradle/gradle/issues/18386
+if [[ " ${@} " =~ " --profile " ]]; then
+ mkdir -p reports
+fi
+
raiseMemory=false
if [[ " ${@} " =~ " -Pandroidx.highMemory " ]]; then
raiseMemory=true
diff --git a/libraryversions.toml b/libraryversions.toml
index 197272c..3cb2699 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -22,7 +22,7 @@
CARDVIEW = "1.1.0-alpha01"
CAR_APP = "1.7.0-alpha01"
COLLECTION = "1.5.0-alpha01"
-COMPOSE = "1.7.0-alpha05"
+COMPOSE = "1.7.0-alpha06"
COMPOSE_COMPILER = "1.5.11" # Update when preparing for a release
COMPOSE_MATERIAL3 = "1.3.0-alpha03"
COMPOSE_MATERIAL3_ADAPTIVE = "1.0.0-alpha09"
@@ -166,7 +166,7 @@
WEAR_TILES = "1.4.0-alpha01"
WEAR_TOOLING_PREVIEW = "1.0.0-rc01"
WEAR_WATCHFACE = "1.3.0-alpha02"
-WEBKIT = "1.11.0-beta01"
+WEBKIT = "1.12.0-alpha01"
# Adding a comment to prevent merge conflicts for Window artifact
WINDOW = "1.3.0-beta01"
WINDOW_EXTENSIONS = "1.3.0-alpha01"
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
index b33cd9b..db7b497 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
@@ -148,7 +148,7 @@
}
assertThat(graph.startDestinationId).isEqualTo(15)
- graph.setStartDestination(TestClass::class)
+ graph.setStartDestination<TestClass>()
assertThat(graph.startDestinationRoute).isEqualTo("route/{arg}")
assertThat(graph.startDestinationId).isEqualTo(serializer<TestClass>().hashCode())
}
@@ -162,7 +162,7 @@
// start destination not added via KClass, cannot match
assertFailsWith<IllegalStateException> {
- graph.setStartDestination(TestClass::class)
+ graph.setStartDestination<TestClass>()
}
}
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
index 7f6ddf4..bff9da7 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
@@ -367,10 +367,9 @@
* @param startDestRoute The route of the destination as a [KClass] to be shown when navigating
* to this NavGraph.
*/
- @OptIn(InternalSerializationApi::class)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public fun setStartDestination(startDestRoute: KClass<*>) {
- setStartDestination(startDestRoute.serializer()) { startDestination ->
+ public inline fun <reified T> setStartDestination() {
+ setStartDestination(serializer<T>()) { startDestination ->
startDestination.route!!
}
}
@@ -394,8 +393,10 @@
}
}
+ // unfortunately needs to be public so reified setStartDestination can access this
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@OptIn(ExperimentalSerializationApi::class)
- private fun <T> setStartDestination(
+ public fun <T> setStartDestination(
serializer: KSerializer<T>,
parseRoute: (NavDestination) -> String,
) {
@@ -403,7 +404,7 @@
val startDest = findNode(id)
checkNotNull(startDest) {
"Cannot find startDestination ${serializer.descriptor.serialName} from NavGraph. " +
- "Ensure the starting NavDestination was added via KClass."
+ "Ensure the starting NavDestination was added with route from KClass."
}
// when dest id is based on serializer, we expect the dest route to have been generated
// and set
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt
index 6e42aff..e4213e9 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt
@@ -21,6 +21,7 @@
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlinx.serialization.InternalSerializationApi
+import kotlinx.serialization.serializer
/**
* Construct a new [NavGraph]
@@ -64,8 +65,8 @@
/**
* Construct a new [NavGraph]
*
- * @param startDestination the starting destination's route as a [KClass] for this NavGraph. The
- * respective NavDestination must be added as a [KClass] in order to match.
+ * @param startDestination the starting destination's route from a [KClass] for this NavGraph. The
+ * respective NavDestination must be added with route from a [KClass] in order to match.
* @param route the graph's unique route as a [KClass]
* @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
* if [route] uses custom NavTypes.
@@ -84,8 +85,8 @@
/**
* Construct a new [NavGraph]
*
- * @param startDestination the starting destination's route as an Object for this NavGraph. The
- * respective NavDestination must be added as a [KClass] in order to match.
+ * @param startDestination the starting destination's route from an Object for this NavGraph. The
+ * respective NavDestination must be added with route from a [KClass] in order to match.
* @param route the graph's unique route as a [KClass]
* @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
* if [route] uses custom NavTypes.
@@ -142,9 +143,9 @@
/**
* Construct a nested [NavGraph]
*
- * @param startDestination the starting destination's route as a [KClass] for this NavGraph. The
- * respective NavDestination must be added as a [KClass] in order to match.
- * @param route the graph's unique route as a [KClass]
+ * @param startDestination the starting destination's route from a [KClass] for this NavGraph. The
+ * respective NavDestination must be added with route from a [KClass] in order to match.
+ * @param route the graph's unique route from a [KClass]
* @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
* if [route] uses custom NavTypes.
*
@@ -161,9 +162,9 @@
/**
* Construct a nested [NavGraph]
*
- * @param startDestination the starting destination's route as an Object for this NavGraph. The
- * respective NavDestination must be added as a [KClass] in order to match.
- * @param route the graph's unique route as a [KClass]
+ * @param startDestination the starting destination's route from an Object for this NavGraph. The
+ * respective NavDestination must be added with route from a [KClass] in order to match.
+ * @param route the graph's unique route from a [KClass]
* @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
* if [route] uses custom NavTypes.
*
@@ -240,7 +241,7 @@
*
* @param provider navigator used to create the destination
* @param startDestination the starting destination's route as a [KClass] for this NavGraph. The
- * respective NavDestination must be added as a [KClass] in order to match.
+ * respective NavDestination must be added with route from a [KClass] in order to match.
* @param route the graph's unique route as a [KClass]
* @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
* if [route] uses custom NavTypes.
@@ -263,7 +264,7 @@
*
* @param provider navigator used to create the destination
* @param startDestination the starting destination's route as an Object for this NavGraph. The
- * respective NavDestination must be added as a [KClass] in order to match.
+ * respective NavDestination must be added with route from a [KClass] in order to match.
* @param route the graph's unique route as a [KClass]
* @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
* if [route] uses custom NavTypes.
@@ -318,7 +319,7 @@
if (startDestinationRoute != null) {
navGraph.setStartDestination(startDestinationRoute!!)
} else if (startDestinationClass != null) {
- navGraph.setStartDestination(startDestinationClass!!)
+ navGraph.setStartDestination(startDestinationClass!!.serializer()) { it.route!! }
} else if (startDestinationObject != null) {
navGraph.setStartDestination(startDestinationObject!!)
} else {
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/current.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/current.xml
index 8c9a6a1..a85bbad 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/current.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/current.xml
@@ -14,6 +14,6 @@
limitations under the License.
-->
<compat-config>
- <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.current.CompatProvider</compat-entrypoint>
+ <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.CompatProvider</compat-entrypoint>
<dex-path>test-sdks/current/classes.dex</dex-path>
</compat-config>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/currentWithResources.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/currentWithResources.xml
index cb99939..8e07975 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/currentWithResources.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/currentWithResources.xml
@@ -14,7 +14,7 @@
limitations under the License.
-->
<compat-config>
- <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.current.CompatProvider</compat-entrypoint>
+ <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.CompatProvider</compat-entrypoint>
<dex-path>test-sdks/current/classes.dex</dex-path>
<dex-path>RuntimeEnabledSdks/RPackage.dex</dex-path>
<java-resources-root-path>RuntimeEnabledSdks/javaresources</java-resources-root-path>
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v1.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v1.xml
index 2bf4d37..38e0309 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v1.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v1.xml
@@ -14,6 +14,6 @@
limitations under the License.
-->
<compat-config>
- <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.v1.CompatProvider</compat-entrypoint>
+ <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.CompatProvider</compat-entrypoint>
<dex-path>test-sdks/v1/classes.dex</dex-path>
</compat-config>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v2.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v2.xml
index ed4f3707..2caa00d 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v2.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v2.xml
@@ -14,6 +14,6 @@
limitations under the License.
-->
<compat-config>
- <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.v2.CompatProvider</compat-entrypoint>
+ <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.CompatProvider</compat-entrypoint>
<dex-path>test-sdks/v2/classes.dex</dex-path>
</compat-config>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v4.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v4.xml
index ea3c856..d1e82e8 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v4.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v4.xml
@@ -14,6 +14,6 @@
limitations under the License.
-->
<compat-config>
- <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.v4.CompatProvider</compat-entrypoint>
+ <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.CompatProvider</compat-entrypoint>
<dex-path>test-sdks/v4/classes.dex</dex-path>
</compat-config>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v5.xml b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v5.xml
index 8d21c64..b8a7dc1 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v5.xml
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/assets/RuntimeEnabledSdks/v5.xml
@@ -14,6 +14,6 @@
limitations under the License.
-->
<compat-config>
- <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.v5.CompatProvider</compat-entrypoint>
+ <compat-entrypoint>androidx.privacysandbox.sdkruntime.testsdk.CompatProvider</compat-entrypoint>
<dex-path>test-sdks/v5/classes.dex</dex-path>
</compat-config>
\ No newline at end of file
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt
index e054456..de29be9 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/config/LocalSdkConfigsHolderTest.kt
@@ -51,7 +51,7 @@
packageName = "androidx.privacysandbox.sdkruntime.testsdk.current",
versionMajor = 42,
dexPaths = listOf("test-sdks/current/classes.dex"),
- entryPoint = "androidx.privacysandbox.sdkruntime.testsdk.current.CompatProvider",
+ entryPoint = "androidx.privacysandbox.sdkruntime.testsdk.CompatProvider",
)
assertThat(result).isEqualTo(expectedConfig)
diff --git a/privacysandbox/sdkruntime/test-sdks/current/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/current/CompatProvider.kt b/privacysandbox/sdkruntime/test-sdks/current/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
similarity index 96%
rename from privacysandbox/sdkruntime/test-sdks/current/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/current/CompatProvider.kt
rename to privacysandbox/sdkruntime/test-sdks/current/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
index c6960bd..fc1f0b5 100644
--- a/privacysandbox/sdkruntime/test-sdks/current/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/current/CompatProvider.kt
+++ b/privacysandbox/sdkruntime/test-sdks/current/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.privacysandbox.sdkruntime.testsdk.current
+package androidx.privacysandbox.sdkruntime.testsdk
import android.content.Context
import android.os.Binder
diff --git a/privacysandbox/sdkruntime/test-sdks/v1/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v1/CompatProvider.kt b/privacysandbox/sdkruntime/test-sdks/v1/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
similarity index 97%
rename from privacysandbox/sdkruntime/test-sdks/v1/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v1/CompatProvider.kt
rename to privacysandbox/sdkruntime/test-sdks/v1/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
index c2b5312..307363e 100644
--- a/privacysandbox/sdkruntime/test-sdks/v1/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v1/CompatProvider.kt
+++ b/privacysandbox/sdkruntime/test-sdks/v1/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.privacysandbox.sdkruntime.testsdk.v1
+package androidx.privacysandbox.sdkruntime.testsdk
import android.content.Context
import android.os.Binder
diff --git a/privacysandbox/sdkruntime/test-sdks/v2/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v2/CompatProvider.kt b/privacysandbox/sdkruntime/test-sdks/v2/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
similarity index 95%
rename from privacysandbox/sdkruntime/test-sdks/v2/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v2/CompatProvider.kt
rename to privacysandbox/sdkruntime/test-sdks/v2/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
index cf4d8c7..8240d3ca 100644
--- a/privacysandbox/sdkruntime/test-sdks/v2/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v2/CompatProvider.kt
+++ b/privacysandbox/sdkruntime/test-sdks/v2/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.privacysandbox.sdkruntime.testsdk.v2
+package androidx.privacysandbox.sdkruntime.testsdk
import android.content.Context
import android.os.Binder
diff --git a/privacysandbox/sdkruntime/test-sdks/v4/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v4/CompatProvider.kt b/privacysandbox/sdkruntime/test-sdks/v4/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
similarity index 96%
rename from privacysandbox/sdkruntime/test-sdks/v4/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v4/CompatProvider.kt
rename to privacysandbox/sdkruntime/test-sdks/v4/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
index a97aec2..e24fb65 100644
--- a/privacysandbox/sdkruntime/test-sdks/v4/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v4/CompatProvider.kt
+++ b/privacysandbox/sdkruntime/test-sdks/v4/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 The Android Open Source Project
+ * Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.privacysandbox.sdkruntime.testsdk.v4
+package androidx.privacysandbox.sdkruntime.testsdk
import android.content.Context
import android.os.Binder
diff --git a/privacysandbox/sdkruntime/test-sdks/v5/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v5/CompatProvider.kt b/privacysandbox/sdkruntime/test-sdks/v5/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
similarity index 98%
rename from privacysandbox/sdkruntime/test-sdks/v5/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v5/CompatProvider.kt
rename to privacysandbox/sdkruntime/test-sdks/v5/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
index bdc8581..857bdc9 100644
--- a/privacysandbox/sdkruntime/test-sdks/v5/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/v5/CompatProvider.kt
+++ b/privacysandbox/sdkruntime/test-sdks/v5/src/main/java/androidx/privacysandbox/sdkruntime/testsdk/CompatProvider.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.privacysandbox.sdkruntime.testsdk.v5
+package androidx.privacysandbox.sdkruntime.testsdk
import android.content.Context
import android.os.Binder
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index 4982912..9ac320a 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -49,6 +49,8 @@
testImplementation(libs.junit)
testImplementation(libs.truth)
testImplementation(libs.kotlinTest)
+ testImplementation(libs.kotlinCoroutinesTest)
+ testImplementation(libs.robolectric)
androidTestImplementation(project(":compose:ui:ui-test"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
@@ -56,6 +58,10 @@
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.kotlinTest)
androidTestImplementation(libs.truth)
+
+ // Includes the wear-sdk jar
+ compileOnly files("../../wear_sdk/wear-sdk.jar")
+ testImplementation(files("../../wear_sdk/wear-sdk.jar"))
}
android {
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Haptics.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Haptics.kt
index 373784a..dd78f66 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Haptics.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/Haptics.kt
@@ -16,6 +16,28 @@
package androidx.wear.compose.foundation.rotary
+import android.content.Context
+import android.os.Build
+import android.provider.Settings
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalView
+import com.google.wear.input.WearHapticFeedbackConstants
+import kotlin.math.abs
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.withContext
+
/**
* Handles haptics for rotary usage
*/
@@ -36,3 +58,313 @@
*/
fun handleLimitHaptic(event: UnifiedRotaryEvent, isStart: Boolean)
}
+
+@Composable
+internal fun rememberRotaryHapticHandler(
+ scrollableState: ScrollableState,
+ hapticsEnabled: Boolean
+): RotaryHapticHandler =
+ if (hapticsEnabled) {
+ // TODO(b/319103162): Add platform haptics once AndroidX updates to Android VanillaIceCream
+ rememberCustomRotaryHapticHandler(scrollableState)
+ } else {
+ rememberDisabledRotaryHapticHandler()
+ }
+
+/**
+ * Remembers custom rotary haptic handler.
+ * @param scrollableState A scrollableState, used to determine whether the end of the scrollable
+ * was reached or not.
+ */
+@Composable
+private fun rememberCustomRotaryHapticHandler(
+ scrollableState: ScrollableState,
+): RotaryHapticHandler {
+ val hapticsProvider = rememberRotaryHapticFeedbackProvider()
+ // Channel to which haptic events will be sent
+ val hapticsChannel: Channel<RotaryHapticsType> = rememberHapticChannel()
+
+ // Throttling events within specified timeframe.
+ // Only first and last events will be received. Check [throttleLatest] function for more info.
+ val throttleThresholdMs: Long = 30
+ // A scroll threshold after which haptic is produced.
+ val hapticsThresholdPx: Long = 50
+
+ LaunchedEffect(hapticsChannel, throttleThresholdMs) {
+ hapticsChannel.receiveAsFlow()
+ .throttleLatest(throttleThresholdMs)
+ .collect { hapticType ->
+ // 'withContext' launches performHapticFeedback in a separate thread,
+ // as otherwise it produces a visible lag (b/219776664)
+ val currentTime = System.currentTimeMillis()
+ debugLog { "Haptics started" }
+ withContext(Dispatchers.Default) {
+ debugLog {
+ "Performing haptics, delay: " +
+ "${System.currentTimeMillis() - currentTime}"
+ }
+ hapticsProvider.performHapticFeedback(hapticType)
+ }
+ }
+ }
+ return remember(scrollableState, hapticsChannel, hapticsProvider) {
+ CustomRotaryHapticHandler(scrollableState, hapticsChannel, hapticsThresholdPx)
+ }
+}
+
+@Composable
+private fun rememberRotaryHapticFeedbackProvider(): RotaryHapticFeedbackProvider =
+ LocalView.current.let { view ->
+ remember {
+ val hapticConstants = getCustomRotaryConstants(view)
+ RotaryHapticFeedbackProvider(view, hapticConstants)
+ }
+ }
+
+@VisibleForTesting
+internal fun getCustomRotaryConstants(view: View): HapticConstants =
+ when {
+ // Order here is very important: We want to use WearSDK haptic constants for
+ // all devices having api 34 and up, but for Wear3.5 and Wear 4 constants should be
+ // different for Galaxy watches and other devices.
+ hasWearSDK() -> HapticConstants.WearSDKHapticConstants
+ isGalaxyWatch() -> HapticConstants.GalaxyWatchConstants
+ isWear3_5(view.context) -> HapticConstants.Wear3Point5RotaryHapticConstants
+ isWear4() -> HapticConstants.Wear4RotaryHapticConstants
+ else -> HapticConstants.DisabledHapticConstants
+ }
+
+@VisibleForTesting
+internal sealed class HapticConstants(
+ val scrollFocus: Int?,
+ val scrollTick: Int?,
+ val scrollLimit: Int?
+) {
+ /**
+ * Rotary haptic constants from WearSDK
+ */
+ object WearSDKHapticConstants : HapticConstants(
+ WearHapticFeedbackConstants.getScrollItemFocus(),
+ WearHapticFeedbackConstants.getScrollTick(),
+ WearHapticFeedbackConstants.getScrollLimit()
+ )
+
+ /**
+ * Rotary haptic constants for Galaxy Watch. These constants
+ * are used by Samsung for producing rotary haptics
+ */
+ object GalaxyWatchConstants : HapticConstants(
+ 102, 101, 50107
+ )
+
+ /**
+ * Hidden constants from HapticFeedbackConstants.java
+ * API 33, Wear 4
+ */
+ object Wear4RotaryHapticConstants : HapticConstants(
+ 19, 18, 20
+ )
+
+ /**
+ * Hidden constants from HapticFeedbackConstants.java
+ * API 30, Wear 3.5
+ */
+ object Wear3Point5RotaryHapticConstants : HapticConstants(
+ 10003, 10002, 10003
+ )
+
+ object DisabledHapticConstants : HapticConstants(
+ null, null, null
+ )
+}
+
+@Composable
+private fun rememberHapticChannel() =
+ remember {
+ Channel<RotaryHapticsType>(
+ capacity = 2,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ }
+
+/**
+ * This class handles haptic feedback based
+ * on the [scrollableState], scrolled pixels and [hapticsThresholdPx].
+ * Haptic is not fired in this class, instead it's sent to [hapticsChannel]
+ * where it'll be performed later.
+ *
+ * @param scrollableState Haptic performed based on this state
+ * @param hapticsChannel Channel to which haptic events will be sent
+ * @param hapticsThresholdPx A scroll threshold after which haptic is produced.
+ */
+private class CustomRotaryHapticHandler(
+ private val scrollableState: ScrollableState,
+ private val hapticsChannel: Channel<RotaryHapticsType>,
+ private val hapticsThresholdPx: Long = 50
+) : RotaryHapticHandler {
+
+ private var overscrollHapticTriggered = false
+ private var currScrollPosition = 0f
+ private var prevHapticsPosition = 0f
+
+ override fun handleScrollHaptic(event: UnifiedRotaryEvent) {
+ if (scrollableState.reachedTheLimit(event.deltaInPixels)) {
+ handleLimitHaptic(event, scrollableState.canScrollBackward)
+ } else {
+ overscrollHapticTriggered = false
+ currScrollPosition += event.deltaInPixels
+ val diff = abs(currScrollPosition - prevHapticsPosition)
+
+ if (diff >= hapticsThresholdPx) {
+ hapticsChannel.trySend(RotaryHapticsType.ScrollTick)
+ prevHapticsPosition = currScrollPosition
+ }
+ }
+ }
+
+ override fun handleSnapHaptic(event: UnifiedRotaryEvent) {
+ if (scrollableState.reachedTheLimit(event.deltaInPixels)) {
+ handleLimitHaptic(event, scrollableState.canScrollBackward)
+ } else {
+ overscrollHapticTriggered = false
+ hapticsChannel.trySend(RotaryHapticsType.ScrollItemFocus)
+ }
+ }
+
+ override fun handleLimitHaptic(event: UnifiedRotaryEvent, isStart: Boolean) {
+ if (!overscrollHapticTriggered) {
+ hapticsChannel.trySend(RotaryHapticsType.ScrollLimit)
+ overscrollHapticTriggered = true
+ }
+ }
+}
+
+/**
+ * Rotary haptic types
+ */
+@JvmInline
+@VisibleForTesting
+internal value class RotaryHapticsType(private val type: Int) {
+ companion object {
+
+ /**
+ * A scroll ticking haptic. Similar to texture haptic - performed each time when
+ * a scrollable content is scrolled by a certain distance
+ */
+ public val ScrollTick: RotaryHapticsType = RotaryHapticsType(1)
+
+ /**
+ * An item focus (snap) haptic. Performed when a scrollable content is snapped
+ * to a specific item.
+ */
+ public val ScrollItemFocus: RotaryHapticsType = RotaryHapticsType(2)
+
+ /**
+ * A limit(overscroll) haptic. Performed when a list reaches the limit
+ * (start or end) and can't scroll further
+ */
+ public val ScrollLimit: RotaryHapticsType = RotaryHapticsType(3)
+ }
+}
+
+/**
+ * Remember disabled haptics handler
+ */
+@Composable
+private fun rememberDisabledRotaryHapticHandler(): RotaryHapticHandler = remember {
+ object : RotaryHapticHandler {
+ override fun handleScrollHaptic(event: UnifiedRotaryEvent) {
+ // Do nothing
+ }
+
+ override fun handleSnapHaptic(event: UnifiedRotaryEvent) {
+ // Do nothing
+ }
+
+ override fun handleLimitHaptic(event: UnifiedRotaryEvent, isStart: Boolean) {
+ // Do nothing
+ }
+ }
+}
+
+/**
+ * Rotary haptic feedback
+ */
+private class RotaryHapticFeedbackProvider(
+ private val view: View,
+ private val hapticConstants: HapticConstants
+) {
+ fun performHapticFeedback(
+ type: RotaryHapticsType,
+ ) {
+ when (type) {
+ RotaryHapticsType.ScrollItemFocus -> {
+ hapticConstants.scrollFocus?.let { view.performHapticFeedback(it) }
+ }
+
+ RotaryHapticsType.ScrollTick -> {
+ hapticConstants.scrollTick?.let { view.performHapticFeedback(it) }
+ }
+
+ RotaryHapticsType.ScrollLimit -> {
+ hapticConstants.scrollLimit?.let { view.performHapticFeedback(it) }
+ }
+ }
+ }
+}
+
+private fun isGalaxyWatch(): Boolean =
+ Build.MANUFACTURER.contains("Samsung", ignoreCase = true) &&
+ Build.MODEL.matches("^SM-R.*\$".toRegex())
+
+private fun isWear3_5(context: Context): Boolean =
+ Build.VERSION.SDK_INT == Build.VERSION_CODES.R && getWearPlatformMrNumber(context) >= 5
+
+private fun isWear4(): Boolean =
+ Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU
+
+private fun hasWearSDK(): Boolean =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+
+private fun getWearPlatformMrNumber(context: Context): Int =
+ Settings.Global
+ .getString(context.contentResolver, WEAR_PLATFORM_MR_NUMBER)?.toIntOrNull() ?: 0
+
+private const val WEAR_PLATFORM_MR_NUMBER: String = "wear_platform_mr_number"
+
+private fun ScrollableState.reachedTheLimit(scrollDelta: Float): Boolean =
+ (scrollDelta > 0 && !canScrollForward) || (scrollDelta < 0 && !canScrollBackward)
+
+/**
+ * Debug logging that can be enabled.
+ */
+private const val DEBUG = false
+
+private inline fun debugLog(generateMsg: () -> String) {
+ if (DEBUG) {
+ println("RotaryHaptics: ${generateMsg()}")
+ }
+}
+
+/**
+ * Throttling events within specified timeframe. Only first and last events will be received.
+ *
+ * For example, a flow emits elements 1 to 30, with a 100ms delay between them:
+ * ```
+ * val flow = flow {
+ * for (i in 1..30) {
+ * delay(100)
+ * emit(i)
+ * }
+ * }
+ * ```
+ * With timeframe=1000 only those integers will be received: 1, 10, 20, 30 .
+ */
+@VisibleForTesting
+internal fun <T> Flow<T>.throttleLatest(timeframe: Long): Flow<T> =
+ flow {
+ conflate().collect {
+ emit(it)
+ delay(timeframe)
+ }
+ }
diff --git a/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/rotary/HapticsTest.kt b/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/rotary/HapticsTest.kt
new file mode 100644
index 0000000..da6eb40
--- /dev/null
+++ b/wear/compose/compose-foundation/src/test/kotlin/androidx/wear/compose/foundation/rotary/HapticsTest.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * 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 androidx.wear.compose.foundation.rotary
+
+import android.R
+import android.app.Activity
+import android.provider.Settings
+import android.view.View
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowBuild
+
+@RunWith(JUnit4::class)
+class ThrottleLatestTest {
+ private lateinit var testChannel: Channel<RotaryHapticsType>
+
+ @Before
+ fun before() {
+ testChannel = Channel(
+ capacity = 10,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ }
+
+ @Test
+ fun single_event_sent() = runTest {
+ val testFlow = testChannel.receiveAsFlow().throttleLatest(40)
+ val expectedItemsSize = 1
+
+ launch {
+ testChannel.trySend(RotaryHapticsType.ScrollTick)
+ testChannel.close()
+ }
+ val actualItems = testFlow.toList()
+
+ assertEquals(expectedItemsSize, actualItems.size)
+ }
+
+ @Test
+ fun three_events_sent_one_filtered() = runTest {
+ val testFlow = testChannel.receiveAsFlow().throttleLatest(40)
+ val expectedItemsSize = 2
+
+ // Send 3 events, receive 2 because they fall into a single timeframe and only
+ // 1st and last items are returned
+ launch {
+ testChannel.sendEventsWithDelay(RotaryHapticsType.ScrollTick, 3, 10)
+ testChannel.close()
+ }
+ val actualItems = testFlow.toList()
+
+ assertEquals(expectedItemsSize, actualItems.size)
+ }
+
+ @Test
+ fun three_events_sent_none_filtered() = runTest {
+ val testFlow = testChannel.receiveAsFlow().throttleLatest(40)
+ val expectedItemsSize = 3
+ // Sent 3 events, received 3 because delay between events is bigger than a timeframe
+ launch {
+ testChannel.sendEventsWithDelay(RotaryHapticsType.ScrollTick, 3, 50)
+ testChannel.close()
+ }
+ val actualItems = testFlow.toList()
+
+ assertEquals(expectedItemsSize, actualItems.size)
+ }
+
+ @Test
+ fun three_slow_and_five_fast() = runTest {
+ val testFlow = testChannel.receiveAsFlow().throttleLatest(40)
+ val expectedItemsSize = 5
+ launch {
+ // Sent 3 events, received 3 because delay between events is bigger than a timeframe
+ testChannel.sendEventsWithDelay(RotaryHapticsType.ScrollTick, 3, 50)
+ delay(50)
+ // Sent 5 events, received 2 (first and last) because delay between events
+ // was smaller than a timeframe
+ testChannel.sendEventsWithDelay(RotaryHapticsType.ScrollTick, 5, 5)
+ delay(5)
+ testChannel.close()
+ }
+
+ val actualItems = testFlow.toList()
+
+ assertEquals(expectedItemsSize, actualItems.size)
+ }
+
+ private suspend fun Channel<RotaryHapticsType>.sendEventsWithDelay(
+ event: RotaryHapticsType,
+ eventCount: Int,
+ delayMillis: Long
+ ) {
+ for (i in 0 until eventCount) {
+ trySend(event)
+ if (i < eventCount - 1) {
+ delay(delayMillis)
+ }
+ }
+ }
+}
+
+@RunWith(RobolectricTestRunner::class)
+class HapticsTest {
+ @Test
+ @Config(sdk = [33])
+ fun testPixelWatch1Wear4() {
+ ShadowBuild.setManufacturer("Google")
+ ShadowBuild.setModel("Google Pixel Watch")
+
+ assertEquals(HapticConstants.Wear4RotaryHapticConstants, getHapticConstants())
+ }
+
+ @Test
+ @Config(sdk = [30])
+ fun testPixelWatch1Wear35() {
+ ShadowBuild.setManufacturer("Google")
+ ShadowBuild.setModel("Google Pixel Watch")
+ Settings.Global.putString(
+ RuntimeEnvironment.getApplication().contentResolver,
+ "wear_platform_mr_number",
+ "5",
+ )
+
+ assertEquals(HapticConstants.Wear3Point5RotaryHapticConstants, getHapticConstants())
+ }
+
+ @Test
+ @Config(sdk = [33])
+ fun testGenericWear4() {
+ ShadowBuild.setManufacturer("XXX")
+ ShadowBuild.setModel("YYY")
+
+ assertEquals(HapticConstants.Wear4RotaryHapticConstants, getHapticConstants())
+ }
+
+ @Test
+ @Config(sdk = [30])
+ fun testGenericWear35() {
+ ShadowBuild.setManufacturer("XXX")
+ ShadowBuild.setModel("YYY")
+ Settings.Global.putString(
+ RuntimeEnvironment.getApplication().contentResolver,
+ "wear_platform_mr_number",
+ "5",
+ )
+
+ assertEquals(HapticConstants.Wear3Point5RotaryHapticConstants, getHapticConstants())
+ }
+
+ @Test
+ @Config(sdk = [30])
+ fun testGenericWear3() {
+ ShadowBuild.setManufacturer("XXX")
+ ShadowBuild.setModel("YYY")
+
+ assertEquals(HapticConstants.DisabledHapticConstants, getHapticConstants())
+ }
+
+ @Test
+ @Config(sdk = [28])
+ fun testGenericWear2() {
+ ShadowBuild.setManufacturer("XXX")
+ ShadowBuild.setModel("YYY")
+
+ assertEquals(HapticConstants.DisabledHapticConstants, getHapticConstants())
+ }
+
+ @Test
+ @Config(sdk = [33])
+ fun testGalaxyWatchClassic() {
+ ShadowBuild.setManufacturer("Samsung")
+ // Galaxy Watch4 Classic
+ ShadowBuild.setModel("SM-R890")
+
+ assertEquals(HapticConstants.GalaxyWatchConstants, getHapticConstants())
+ }
+
+ @Test
+ @Config(sdk = [33])
+ fun testGalaxyWatch() {
+ ShadowBuild.setManufacturer("Samsung")
+ // Galaxy Watch 5 Pro
+ ShadowBuild.setModel("SM-R925")
+
+ assertEquals(HapticConstants.GalaxyWatchConstants, getHapticConstants())
+ }
+
+ private fun getHapticConstants(): HapticConstants {
+ val activity = Robolectric.buildActivity(Activity::class.java).get()
+ val view = activity.findViewById<View>(R.id.content)
+
+ return getCustomRotaryConstants(view)
+ }
+}