Merge "Remove multiple passes of drag." into androidx-master-dev
diff --git a/.github/workflows/presubmit.yml b/.github/workflows/presubmit.yml
index 27801a3b4..15e5c1e 100644
--- a/.github/workflows/presubmit.yml
+++ b/.github/workflows/presubmit.yml
@@ -1,6 +1,8 @@
 name: AndroidX Presubmits
 on:
   push:
+  pull_request_target:
+    types: [opened, synchronize, reopened]
   pull_request:
     types: [opened, synchronize, reopened]
   pull_request_review:
diff --git a/activity/activity/src/main/java/androidx/activity/ComponentActivity.java b/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
index 93ea754..f289076 100644
--- a/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
+++ b/activity/activity/src/main/java/androidx/activity/ComponentActivity.java
@@ -165,6 +165,7 @@
             Bundle optionsBundle = null;
             if (intent.hasExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE)) {
                 optionsBundle = intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE);
+                intent.removeExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE);
             } else if (options != null) {
                 optionsBundle = options.toBundle();
             }
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReceiveResultTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReceiveResultTest.kt
index c786445..be97a16 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReceiveResultTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReceiveResultTest.kt
@@ -19,6 +19,7 @@
 import android.app.PendingIntent
 import android.content.Intent
 import android.content.IntentSender
+import android.os.Bundle
 import androidx.fragment.app.test.FragmentResultActivity
 import androidx.fragment.app.test.FragmentTestActivity
 import androidx.fragment.test.R
@@ -119,6 +120,16 @@
     }
 
     @Test
+    fun testStartIntentSenderForResultWithOptionsOk() {
+        startIntentSenderForResult(30, Activity.RESULT_OK, "content 30", Bundle())
+
+        assertWithMessage("Fragment should receive result").that(fragment.hasResult[0]).isTrue()
+        assertThat(fragment.requestCode[0]).isEqualTo(30)
+        assertThat(fragment.resultCode[0]).isEqualTo(Activity.RESULT_OK)
+        assertThat(fragment.resultContent[0]).isEqualTo("content 30")
+    }
+
+    @Test
     fun testStartIntentSenderForResultCanceled() {
         startIntentSenderForResult(40, Activity.RESULT_CANCELED, "content 40")
 
@@ -163,7 +174,8 @@
     private fun startIntentSenderForResult(
         requestCode: Int,
         resultCode: Int,
-        content: String
+        content: String,
+        options: Bundle? = null
     ) {
         activityRule.runOnUiThread {
             val intent = Intent(activity, FragmentResultActivity::class.java)
@@ -175,7 +187,7 @@
             try {
                 fragment.startIntentSenderForResult(
                     pendingIntent.intentSender,
-                    requestCode, null, 0, 0, 0, null
+                    requestCode, null, 0, 0, 0, options
                 )
             } catch (e: IntentSender.SendIntentException) {
                 fail("IntentSender failed")
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
index d98909d..9d98e62 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -1440,6 +1440,11 @@
         if (mHost == null) {
             throw new IllegalStateException("Fragment " + this + " not attached to Activity");
         }
+        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+            Log.v(FragmentManager.TAG, "Fragment " + this + " received the following in "
+                    + "startIntentSenderForResult() requestCode: " + requestCode + " IntentSender: "
+                    + intent + " fillInIntent: " + fillInIntent + " options: " + options);
+        }
         getParentFragmentManager().launchStartIntentSenderForResult(this, intent, requestCode,
                 fillInIntent, flagsMask, flagsValues, extraFlags, options);
     }
@@ -1466,6 +1471,11 @@
     @SuppressWarnings("DeprecatedIsStillUsed")
     @Deprecated
     public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+        if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
+            Log.v(FragmentManager.TAG, "Fragment " + this + " received the following in "
+                    + "onActivityResult(): requestCode: " + requestCode + " resultCode: "
+                    + resultCode + " data: " + data);
+        }
     }
 
     /**
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 1d5007f..01bf8c3 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -493,15 +493,15 @@
                 }
             };
 
-    // Key to retrieve the request code from any intents that are started
-    private static final String EXTRA_KEY_REQUEST_CODE = "activity.result.requestCode";
-
     private ActivityResultLauncher<Intent> mStartActivityForResult;
     private ActivityResultLauncher<IntentSenderRequest> mStartIntentSenderForResult;
     private ActivityResultLauncher<String[]> mRequestPermissions;
 
     ArrayDeque<LaunchedFragmentInfo> mLaunchedFragments = new ArrayDeque<>();
 
+    private static final String EXTRA_CREATED_FILLIN_INTENT = "androidx.fragment"
+            + ".extra.ACTIVITY_OPTIONS_BUNDLE";
+
     private boolean mNeedMenuInvalidate;
     private boolean mStateSaved;
     private boolean mStopped;
@@ -2906,8 +2906,7 @@
                             // fragment transactions was committed immediately after the for
                             // result call
                             if (fragment == null) {
-                                Log.w(TAG,
-                                        "Intent Sender result delivered for unknown Fragment "
+                                Log.w(TAG, "Intent Sender result delivered for unknown Fragment "
                                                 + fragmentWho);
                                 return;
                             }
@@ -2976,7 +2975,7 @@
         if (mStartActivityForResult != null) {
             LaunchedFragmentInfo info = new LaunchedFragmentInfo(f.mWho, requestCode);
             mLaunchedFragments.addLast(info);
-            if (options != null) {
+            if (intent != null && options != null) {
                 intent.putExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE, options);
             }
             mStartActivityForResult.launch(intent);
@@ -2994,6 +2993,11 @@
             if (options != null) {
                 if (fillInIntent == null) {
                     fillInIntent = new Intent();
+                    fillInIntent.putExtra(EXTRA_CREATED_FILLIN_INTENT, true);
+                }
+                if (isLoggingEnabled(Log.VERBOSE)) {
+                    Log.v(TAG, "ActivityOptions " + options + " were added to fillInIntent "
+                            + fillInIntent + " for fragment " + f);
                 }
                 fillInIntent.putExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE, options);
             }
@@ -3002,6 +3006,9 @@
                             .setFlags(flagsValues, flagsMask).build();
             LaunchedFragmentInfo info = new LaunchedFragmentInfo(f.mWho, requestCode);
             mLaunchedFragments.addLast(info);
+            if (isLoggingEnabled(Log.VERBOSE)) {
+                Log.v(TAG, "Fragment " + f + "is launching an IntentSender for result ");
+            }
             mStartIntentSenderForResult.launch(request);
         } else {
             mHost.onStartIntentSenderFromFragment(f, intent, requestCode, fillInIntent,
@@ -3628,17 +3635,24 @@
         @Override
         public Intent createIntent(@NonNull Context context, IntentSenderRequest input) {
             Intent result = new Intent(ACTION_INTENT_SENDER_REQUEST);
-            if (input.getFillInIntent() != null) {
-                Bundle activityOptions =
-                        input.getFillInIntent().getBundleExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE);
-                int requestCode =
-                        input.getFillInIntent().getIntExtra(EXTRA_KEY_REQUEST_CODE, -1);
+            Intent fillInIntent = input.getFillInIntent();
+            if (fillInIntent != null) {
+                Bundle activityOptions = fillInIntent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE);
                 if (activityOptions != null) {
-                    result.putExtra(EXTRA_KEY_REQUEST_CODE, requestCode);
                     result.putExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE, activityOptions);
+                    fillInIntent.removeExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE);
+                    if (fillInIntent.getBooleanExtra(EXTRA_CREATED_FILLIN_INTENT, false)) {
+                        input = new IntentSenderRequest.Builder(input.getIntentSender())
+                                .setFillInIntent(null)
+                                .setFlags(input.getFlagsValues(), input.getFlagsMask())
+                                .build();
+                    }
                 }
             }
             result.putExtra(EXTRA_INTENT_SENDER_REQUEST, input);
+            if (isLoggingEnabled(Log.VERBOSE)) {
+                Log.v(TAG, "CreateIntent created the following intent: " + result);
+            }
             return result;
         }
 
diff --git a/ui/ui-material/api/current.txt b/ui/ui-material/api/current.txt
index 6acb362..4c4a03d 100644
--- a/ui/ui-material/api/current.txt
+++ b/ui/ui-material/api/current.txt
@@ -202,11 +202,22 @@
   @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterialApi {
   }
 
+  @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public final class FixedThreshold implements androidx.compose.material.ThresholdConfig {
+    method public float computeThreshold(androidx.compose.ui.unit.Density, float fromValue, float toValue);
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FixedThreshold copy-0680j_4(float offset);
+  }
+
   public final class FloatingActionButtonKt {
     method @androidx.compose.runtime.Composable public static void ExtendedFloatingActionButton-NLuz2VQ(kotlin.jvm.functions.Function0<kotlin.Unit> text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.ui.Modifier modifier = Modifier, kotlin.jvm.functions.Function0<kotlin.Unit>? icon = null, androidx.compose.ui.graphics.Shape shape = MaterialTheme.shapes.small.copy(CornerSize(50)), long backgroundColor = MaterialTheme.colors.secondary, long contentColor = contentColorFor(backgroundColor), float elevation = 6.dp);
     method @androidx.compose.runtime.Composable public static void FloatingActionButton-NGcTDU4(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.ui.Modifier modifier = Modifier, androidx.compose.ui.graphics.Shape shape = MaterialTheme.shapes.small.copy(CornerSize(50)), long backgroundColor = MaterialTheme.colors.secondary, long contentColor = contentColorFor(backgroundColor), float elevation = 6.dp, kotlin.jvm.functions.Function0<kotlin.Unit> icon);
   }
 
+  @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public final class FractionalThreshold implements androidx.compose.material.ThresholdConfig {
+    ctor public FractionalThreshold(@FloatRange(from=null, to=null) float fraction);
+    method public float computeThreshold(androidx.compose.ui.unit.Density, float fromValue, float toValue);
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FractionalThreshold copy(float fraction);
+  }
+
   public final class IconButtonKt {
     method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.ui.Modifier modifier = Modifier, kotlin.jvm.functions.Function0<kotlin.Unit> icon);
     method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.compose.ui.Modifier modifier = Modifier, kotlin.jvm.functions.Function0<kotlin.Unit> icon);
@@ -351,14 +362,12 @@
   }
 
   public final class SwipeToDismissKt {
-    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material.DismissState state, androidx.compose.ui.Modifier modifier = Modifier, java.util.Set<? extends androidx.compose.material.DismissDirection> directions = setOf(EndToStart, StartToEnd), kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent);
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material.DismissState state, androidx.compose.ui.Modifier modifier = Modifier, java.util.Set<? extends androidx.compose.material.DismissDirection> directions = setOf(EndToStart, StartToEnd), kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissDirection,? extends androidx.compose.material.ThresholdConfig> dismissThresholds = { return <init>(0.5) }, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent);
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.DismissState rememberDismissState(androidx.compose.material.DismissValue initialValue = androidx.compose.material.DismissValue.Default, kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissValue,java.lang.Boolean> confirmStateChange = { return true });
   }
 
   public final class SwipeableKt {
-    method @androidx.compose.material.ExperimentalMaterialApi public static kotlin.jvm.functions.Function3<androidx.compose.ui.unit.Density,java.lang.Float,java.lang.Float,java.lang.Float> fixedThresholds-0680j_4(float offset);
-    method @androidx.compose.material.ExperimentalMaterialApi public static kotlin.jvm.functions.Function3<androidx.compose.ui.unit.Density,java.lang.Float,java.lang.Float,java.lang.Float> fractionalThresholds(@FloatRange(from=0.0, to=1.0) float fraction);
-    method @androidx.compose.material.ExperimentalMaterialApi public static <T> androidx.compose.ui.Modifier swipeable(androidx.compose.ui.Modifier, androidx.compose.material.SwipeableState<T> state, java.util.Map<java.lang.Float,? extends T> anchors, kotlin.jvm.functions.Function3<? super androidx.compose.ui.unit.Density,? super java.lang.Float,? super java.lang.Float,java.lang.Float> thresholds, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation, boolean enabled = true, boolean reverseDirection = false, float minValue = elvis {
+    method @androidx.compose.material.ExperimentalMaterialApi public static <T> androidx.compose.ui.Modifier swipeable(androidx.compose.ui.Modifier, androidx.compose.material.SwipeableState<T> state, java.util.Map<java.lang.Float,? extends T> anchors, kotlin.jvm.functions.Function2<? super T,? super T,? extends androidx.compose.material.ThresholdConfig> thresholds, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation, boolean enabled = true, boolean reverseDirection = false, float minValue = elvis {
     var varf5a37bc5: <ErrorType> = anchors.keys.<anonymous class>()
     if (varf5a37bc5 != null) varf5a37bc5 else Float.NEGATIVE_INFINITY
 }, float maxValue = elvis {
@@ -433,6 +442,10 @@
     method @androidx.compose.runtime.Composable public static void TextField-pcAQjko(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, kotlin.jvm.functions.Function0<kotlin.Unit> label, androidx.compose.ui.Modifier modifier = Modifier, androidx.compose.ui.text.TextStyle textStyle = currentTextStyle(), kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder = null, kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon = null, kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon = null, boolean isErrorValue = false, androidx.compose.ui.text.input.VisualTransformation visualTransformation = VisualTransformation.None, androidx.compose.ui.text.input.KeyboardType keyboardType = KeyboardType.Text, androidx.compose.ui.text.input.ImeAction imeAction = ImeAction.Unspecified, kotlin.jvm.functions.Function2<? super androidx.compose.ui.text.input.ImeAction,? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onImeActionPerformed = { _, _ ->  }, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onFocusChanged = {}, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted = {}, long activeColor = MaterialTheme.colors.primary, long inactiveColor = MaterialTheme.colors.onSurface, long errorColor = MaterialTheme.colors.error, long backgroundColor = MaterialTheme.colors.onSurface, androidx.compose.ui.graphics.Shape shape = MaterialTheme.shapes.small.copy(ZeroCornerSize, ZeroCornerSize));
   }
 
+  @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface ThresholdConfig {
+    method public float computeThreshold(androidx.compose.ui.unit.Density, float fromValue, float toValue);
+  }
+
   @androidx.compose.runtime.Immutable public final class Typography {
     ctor public Typography(androidx.compose.ui.text.font.FontFamily defaultFontFamily, androidx.compose.ui.text.TextStyle h1, androidx.compose.ui.text.TextStyle h2, androidx.compose.ui.text.TextStyle h3, androidx.compose.ui.text.TextStyle h4, androidx.compose.ui.text.TextStyle h5, androidx.compose.ui.text.TextStyle h6, androidx.compose.ui.text.TextStyle subtitle1, androidx.compose.ui.text.TextStyle subtitle2, androidx.compose.ui.text.TextStyle body1, androidx.compose.ui.text.TextStyle body2, androidx.compose.ui.text.TextStyle button, androidx.compose.ui.text.TextStyle caption, androidx.compose.ui.text.TextStyle overline);
     method public androidx.compose.ui.text.TextStyle component1();
diff --git a/ui/ui-material/api/public_plus_experimental_current.txt b/ui/ui-material/api/public_plus_experimental_current.txt
index 6acb362..4c4a03d 100644
--- a/ui/ui-material/api/public_plus_experimental_current.txt
+++ b/ui/ui-material/api/public_plus_experimental_current.txt
@@ -202,11 +202,22 @@
   @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterialApi {
   }
 
+  @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public final class FixedThreshold implements androidx.compose.material.ThresholdConfig {
+    method public float computeThreshold(androidx.compose.ui.unit.Density, float fromValue, float toValue);
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FixedThreshold copy-0680j_4(float offset);
+  }
+
   public final class FloatingActionButtonKt {
     method @androidx.compose.runtime.Composable public static void ExtendedFloatingActionButton-NLuz2VQ(kotlin.jvm.functions.Function0<kotlin.Unit> text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.ui.Modifier modifier = Modifier, kotlin.jvm.functions.Function0<kotlin.Unit>? icon = null, androidx.compose.ui.graphics.Shape shape = MaterialTheme.shapes.small.copy(CornerSize(50)), long backgroundColor = MaterialTheme.colors.secondary, long contentColor = contentColorFor(backgroundColor), float elevation = 6.dp);
     method @androidx.compose.runtime.Composable public static void FloatingActionButton-NGcTDU4(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.ui.Modifier modifier = Modifier, androidx.compose.ui.graphics.Shape shape = MaterialTheme.shapes.small.copy(CornerSize(50)), long backgroundColor = MaterialTheme.colors.secondary, long contentColor = contentColorFor(backgroundColor), float elevation = 6.dp, kotlin.jvm.functions.Function0<kotlin.Unit> icon);
   }
 
+  @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public final class FractionalThreshold implements androidx.compose.material.ThresholdConfig {
+    ctor public FractionalThreshold(@FloatRange(from=null, to=null) float fraction);
+    method public float computeThreshold(androidx.compose.ui.unit.Density, float fromValue, float toValue);
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FractionalThreshold copy(float fraction);
+  }
+
   public final class IconButtonKt {
     method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.ui.Modifier modifier = Modifier, kotlin.jvm.functions.Function0<kotlin.Unit> icon);
     method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.compose.ui.Modifier modifier = Modifier, kotlin.jvm.functions.Function0<kotlin.Unit> icon);
@@ -351,14 +362,12 @@
   }
 
   public final class SwipeToDismissKt {
-    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material.DismissState state, androidx.compose.ui.Modifier modifier = Modifier, java.util.Set<? extends androidx.compose.material.DismissDirection> directions = setOf(EndToStart, StartToEnd), kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent);
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material.DismissState state, androidx.compose.ui.Modifier modifier = Modifier, java.util.Set<? extends androidx.compose.material.DismissDirection> directions = setOf(EndToStart, StartToEnd), kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissDirection,? extends androidx.compose.material.ThresholdConfig> dismissThresholds = { return <init>(0.5) }, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent);
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.DismissState rememberDismissState(androidx.compose.material.DismissValue initialValue = androidx.compose.material.DismissValue.Default, kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissValue,java.lang.Boolean> confirmStateChange = { return true });
   }
 
   public final class SwipeableKt {
-    method @androidx.compose.material.ExperimentalMaterialApi public static kotlin.jvm.functions.Function3<androidx.compose.ui.unit.Density,java.lang.Float,java.lang.Float,java.lang.Float> fixedThresholds-0680j_4(float offset);
-    method @androidx.compose.material.ExperimentalMaterialApi public static kotlin.jvm.functions.Function3<androidx.compose.ui.unit.Density,java.lang.Float,java.lang.Float,java.lang.Float> fractionalThresholds(@FloatRange(from=0.0, to=1.0) float fraction);
-    method @androidx.compose.material.ExperimentalMaterialApi public static <T> androidx.compose.ui.Modifier swipeable(androidx.compose.ui.Modifier, androidx.compose.material.SwipeableState<T> state, java.util.Map<java.lang.Float,? extends T> anchors, kotlin.jvm.functions.Function3<? super androidx.compose.ui.unit.Density,? super java.lang.Float,? super java.lang.Float,java.lang.Float> thresholds, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation, boolean enabled = true, boolean reverseDirection = false, float minValue = elvis {
+    method @androidx.compose.material.ExperimentalMaterialApi public static <T> androidx.compose.ui.Modifier swipeable(androidx.compose.ui.Modifier, androidx.compose.material.SwipeableState<T> state, java.util.Map<java.lang.Float,? extends T> anchors, kotlin.jvm.functions.Function2<? super T,? super T,? extends androidx.compose.material.ThresholdConfig> thresholds, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation, boolean enabled = true, boolean reverseDirection = false, float minValue = elvis {
     var varf5a37bc5: <ErrorType> = anchors.keys.<anonymous class>()
     if (varf5a37bc5 != null) varf5a37bc5 else Float.NEGATIVE_INFINITY
 }, float maxValue = elvis {
@@ -433,6 +442,10 @@
     method @androidx.compose.runtime.Composable public static void TextField-pcAQjko(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, kotlin.jvm.functions.Function0<kotlin.Unit> label, androidx.compose.ui.Modifier modifier = Modifier, androidx.compose.ui.text.TextStyle textStyle = currentTextStyle(), kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder = null, kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon = null, kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon = null, boolean isErrorValue = false, androidx.compose.ui.text.input.VisualTransformation visualTransformation = VisualTransformation.None, androidx.compose.ui.text.input.KeyboardType keyboardType = KeyboardType.Text, androidx.compose.ui.text.input.ImeAction imeAction = ImeAction.Unspecified, kotlin.jvm.functions.Function2<? super androidx.compose.ui.text.input.ImeAction,? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onImeActionPerformed = { _, _ ->  }, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onFocusChanged = {}, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted = {}, long activeColor = MaterialTheme.colors.primary, long inactiveColor = MaterialTheme.colors.onSurface, long errorColor = MaterialTheme.colors.error, long backgroundColor = MaterialTheme.colors.onSurface, androidx.compose.ui.graphics.Shape shape = MaterialTheme.shapes.small.copy(ZeroCornerSize, ZeroCornerSize));
   }
 
+  @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface ThresholdConfig {
+    method public float computeThreshold(androidx.compose.ui.unit.Density, float fromValue, float toValue);
+  }
+
   @androidx.compose.runtime.Immutable public final class Typography {
     ctor public Typography(androidx.compose.ui.text.font.FontFamily defaultFontFamily, androidx.compose.ui.text.TextStyle h1, androidx.compose.ui.text.TextStyle h2, androidx.compose.ui.text.TextStyle h3, androidx.compose.ui.text.TextStyle h4, androidx.compose.ui.text.TextStyle h5, androidx.compose.ui.text.TextStyle h6, androidx.compose.ui.text.TextStyle subtitle1, androidx.compose.ui.text.TextStyle subtitle2, androidx.compose.ui.text.TextStyle body1, androidx.compose.ui.text.TextStyle body2, androidx.compose.ui.text.TextStyle button, androidx.compose.ui.text.TextStyle caption, androidx.compose.ui.text.TextStyle overline);
     method public androidx.compose.ui.text.TextStyle component1();
diff --git a/ui/ui-material/api/restricted_current.txt b/ui/ui-material/api/restricted_current.txt
index 6acb362..4c4a03d 100644
--- a/ui/ui-material/api/restricted_current.txt
+++ b/ui/ui-material/api/restricted_current.txt
@@ -202,11 +202,22 @@
   @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterialApi {
   }
 
+  @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public final class FixedThreshold implements androidx.compose.material.ThresholdConfig {
+    method public float computeThreshold(androidx.compose.ui.unit.Density, float fromValue, float toValue);
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FixedThreshold copy-0680j_4(float offset);
+  }
+
   public final class FloatingActionButtonKt {
     method @androidx.compose.runtime.Composable public static void ExtendedFloatingActionButton-NLuz2VQ(kotlin.jvm.functions.Function0<kotlin.Unit> text, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.ui.Modifier modifier = Modifier, kotlin.jvm.functions.Function0<kotlin.Unit>? icon = null, androidx.compose.ui.graphics.Shape shape = MaterialTheme.shapes.small.copy(CornerSize(50)), long backgroundColor = MaterialTheme.colors.secondary, long contentColor = contentColorFor(backgroundColor), float elevation = 6.dp);
     method @androidx.compose.runtime.Composable public static void FloatingActionButton-NGcTDU4(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.ui.Modifier modifier = Modifier, androidx.compose.ui.graphics.Shape shape = MaterialTheme.shapes.small.copy(CornerSize(50)), long backgroundColor = MaterialTheme.colors.secondary, long contentColor = contentColorFor(backgroundColor), float elevation = 6.dp, kotlin.jvm.functions.Function0<kotlin.Unit> icon);
   }
 
+  @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public final class FractionalThreshold implements androidx.compose.material.ThresholdConfig {
+    ctor public FractionalThreshold(@FloatRange(from=null, to=null) float fraction);
+    method public float computeThreshold(androidx.compose.ui.unit.Density, float fromValue, float toValue);
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FractionalThreshold copy(float fraction);
+  }
+
   public final class IconButtonKt {
     method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.ui.Modifier modifier = Modifier, kotlin.jvm.functions.Function0<kotlin.Unit> icon);
     method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, boolean enabled = true, androidx.compose.ui.Modifier modifier = Modifier, kotlin.jvm.functions.Function0<kotlin.Unit> icon);
@@ -351,14 +362,12 @@
   }
 
   public final class SwipeToDismissKt {
-    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material.DismissState state, androidx.compose.ui.Modifier modifier = Modifier, java.util.Set<? extends androidx.compose.material.DismissDirection> directions = setOf(EndToStart, StartToEnd), kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent);
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void SwipeToDismiss(androidx.compose.material.DismissState state, androidx.compose.ui.Modifier modifier = Modifier, java.util.Set<? extends androidx.compose.material.DismissDirection> directions = setOf(EndToStart, StartToEnd), kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissDirection,? extends androidx.compose.material.ThresholdConfig> dismissThresholds = { return <init>(0.5) }, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> background, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> dismissContent);
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.DismissState rememberDismissState(androidx.compose.material.DismissValue initialValue = androidx.compose.material.DismissValue.Default, kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissValue,java.lang.Boolean> confirmStateChange = { return true });
   }
 
   public final class SwipeableKt {
-    method @androidx.compose.material.ExperimentalMaterialApi public static kotlin.jvm.functions.Function3<androidx.compose.ui.unit.Density,java.lang.Float,java.lang.Float,java.lang.Float> fixedThresholds-0680j_4(float offset);
-    method @androidx.compose.material.ExperimentalMaterialApi public static kotlin.jvm.functions.Function3<androidx.compose.ui.unit.Density,java.lang.Float,java.lang.Float,java.lang.Float> fractionalThresholds(@FloatRange(from=0.0, to=1.0) float fraction);
-    method @androidx.compose.material.ExperimentalMaterialApi public static <T> androidx.compose.ui.Modifier swipeable(androidx.compose.ui.Modifier, androidx.compose.material.SwipeableState<T> state, java.util.Map<java.lang.Float,? extends T> anchors, kotlin.jvm.functions.Function3<? super androidx.compose.ui.unit.Density,? super java.lang.Float,? super java.lang.Float,java.lang.Float> thresholds, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation, boolean enabled = true, boolean reverseDirection = false, float minValue = elvis {
+    method @androidx.compose.material.ExperimentalMaterialApi public static <T> androidx.compose.ui.Modifier swipeable(androidx.compose.ui.Modifier, androidx.compose.material.SwipeableState<T> state, java.util.Map<java.lang.Float,? extends T> anchors, kotlin.jvm.functions.Function2<? super T,? super T,? extends androidx.compose.material.ThresholdConfig> thresholds, androidx.compose.ui.gesture.scrollorientationlocking.Orientation orientation, boolean enabled = true, boolean reverseDirection = false, float minValue = elvis {
     var varf5a37bc5: <ErrorType> = anchors.keys.<anonymous class>()
     if (varf5a37bc5 != null) varf5a37bc5 else Float.NEGATIVE_INFINITY
 }, float maxValue = elvis {
@@ -433,6 +442,10 @@
     method @androidx.compose.runtime.Composable public static void TextField-pcAQjko(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, kotlin.jvm.functions.Function0<kotlin.Unit> label, androidx.compose.ui.Modifier modifier = Modifier, androidx.compose.ui.text.TextStyle textStyle = currentTextStyle(), kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder = null, kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon = null, kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon = null, boolean isErrorValue = false, androidx.compose.ui.text.input.VisualTransformation visualTransformation = VisualTransformation.None, androidx.compose.ui.text.input.KeyboardType keyboardType = KeyboardType.Text, androidx.compose.ui.text.input.ImeAction imeAction = ImeAction.Unspecified, kotlin.jvm.functions.Function2<? super androidx.compose.ui.text.input.ImeAction,? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onImeActionPerformed = { _, _ ->  }, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onFocusChanged = {}, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted = {}, long activeColor = MaterialTheme.colors.primary, long inactiveColor = MaterialTheme.colors.onSurface, long errorColor = MaterialTheme.colors.error, long backgroundColor = MaterialTheme.colors.onSurface, androidx.compose.ui.graphics.Shape shape = MaterialTheme.shapes.small.copy(ZeroCornerSize, ZeroCornerSize));
   }
 
+  @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public interface ThresholdConfig {
+    method public float computeThreshold(androidx.compose.ui.unit.Density, float fromValue, float toValue);
+  }
+
   @androidx.compose.runtime.Immutable public final class Typography {
     ctor public Typography(androidx.compose.ui.text.font.FontFamily defaultFontFamily, androidx.compose.ui.text.TextStyle h1, androidx.compose.ui.text.TextStyle h2, androidx.compose.ui.text.TextStyle h3, androidx.compose.ui.text.TextStyle h4, androidx.compose.ui.text.TextStyle h5, androidx.compose.ui.text.TextStyle h6, androidx.compose.ui.text.TextStyle subtitle1, androidx.compose.ui.text.TextStyle subtitle2, androidx.compose.ui.text.TextStyle body1, androidx.compose.ui.text.TextStyle body2, androidx.compose.ui.text.TextStyle button, androidx.compose.ui.text.TextStyle caption, androidx.compose.ui.text.TextStyle overline);
     method public androidx.compose.ui.text.TextStyle component1();
diff --git a/ui/ui-material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt b/ui/ui-material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
index 27fdafb..f628627 100644
--- a/ui/ui-material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
+++ b/ui/ui-material/samples/src/main/java/androidx/compose/material/samples/SwipeToDismissSamples.kt
@@ -32,6 +32,7 @@
 import androidx.compose.material.DismissValue.DismissedToEnd
 import androidx.compose.material.DismissValue.DismissedToStart
 import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.FractionalThreshold
 import androidx.compose.material.ListItem
 import androidx.compose.material.SwipeToDismiss
 import androidx.compose.material.icons.Icons
@@ -71,6 +72,9 @@
             state = dismissState,
             modifier = Modifier.padding(vertical = 4.dp),
             directions = setOf(StartToEnd, EndToStart),
+            dismissThresholds = { direction ->
+                FractionalThreshold(if (direction == StartToEnd) 0.25f else 0.5f)
+            },
             background = {
                 val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
                 val color = animate(when (dismissState.swipeTarget) {
diff --git a/ui/ui-material/samples/src/main/java/androidx/compose/material/samples/SwipeableSamples.kt b/ui/ui-material/samples/src/main/java/androidx/compose/material/samples/SwipeableSamples.kt
index 76287e5..3cd2cc0 100644
--- a/ui/ui-material/samples/src/main/java/androidx/compose/material/samples/SwipeableSamples.kt
+++ b/ui/ui-material/samples/src/main/java/androidx/compose/material/samples/SwipeableSamples.kt
@@ -25,8 +25,8 @@
 import androidx.compose.foundation.layout.preferredSize
 import androidx.compose.foundation.layout.preferredWidth
 import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.FractionalThreshold
 import androidx.compose.material.SwipeableState
-import androidx.compose.material.fractionalThresholds
 import androidx.compose.material.swipeable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
@@ -61,7 +61,7 @@
             .swipeable(
                 state = swipeableState,
                 anchors = anchors,
-                thresholds = fractionalThresholds(0.5f),
+                thresholds = { _, _ -> FractionalThreshold(0.5f) },
                 orientation = Orientation.Horizontal
             ),
         backgroundColor = Color.Black
diff --git a/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt b/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
index 12bfe4c..edc1c2a 100644
--- a/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
+++ b/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
@@ -306,7 +306,7 @@
             Modifier.swipeable(
                 state = drawerState,
                 anchors = anchors,
-                thresholds = fractionalThresholds(0.5f),
+                thresholds = { _, _ -> FractionalThreshold(0.5f) },
                 orientation = Orientation.Horizontal,
                 enabled = gesturesEnabled,
                 reverseDirection = isRtl
@@ -417,7 +417,7 @@
             Modifier.swipeable(
                 state = drawerState,
                 anchors = anchors,
-                thresholds = fixedThresholds(BottomDrawerThreshold),
+                thresholds = { _, _ -> FixedThreshold(BottomDrawerThreshold) },
                 orientation = Orientation.Vertical,
                 enabled = gesturesEnabled
             )
diff --git a/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt b/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt
index 34d1d66..0a81e25 100644
--- a/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt
+++ b/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/SwipeToDismiss.kt
@@ -90,26 +90,7 @@
      * change the background of the [SwipeToDismiss] if you want different actions on each side.
      */
     val dismissDirection: DismissDirection?
-        get() {
-            val (from, to, _) = swipeProgress
-            return when {
-                // settled at the default state
-                from == to && from == Default -> null
-                // has been dismissed to the end
-                from == to && from == DismissedToEnd -> StartToEnd
-                // has been dismissed to the start
-                from == to && from == DismissedToStart -> EndToStart
-                // is currently being dismissed to the end
-                from == Default && to == DismissedToEnd -> StartToEnd
-                // is currently being dismissed to the start
-                from == Default && to == DismissedToStart -> EndToStart
-                // has been dismissed to the end but is now animated back to default
-                from == DismissedToEnd && to == Default -> StartToEnd
-                // has been dismissed to the start but is now animated back to default
-                from == DismissedToStart && to == Default -> EndToStart
-                else -> null
-            }
-        }
+        get() = getDismissDirection(swipeProgress.from, swipeProgress.to)
 
     /**
      * Whether the component has been dismissed in the given [direction].
@@ -173,6 +154,7 @@
  * @param state The state of this component.
  * @param modifier Optional [Modifier] for this component.
  * @param directions The set of directions in which the component can be dismissed.
+ * @param dismissThresholds The thresholds the item needs to be swiped in order to be dismissed.
  * @param background A composable that is stacked behind the content and is exposed when the
  * content is swiped. You can/should use the [state] to have different backgrounds on each side.
  * @param dismissContent The content that can be dismissed.
@@ -183,6 +165,7 @@
     state: DismissState,
     modifier: Modifier = Modifier,
     directions: Set<DismissDirection> = setOf(EndToStart, StartToEnd),
+    dismissThresholds: (DismissDirection) -> ThresholdConfig = { FractionalThreshold(0.5f) },
     background: @Composable RowScope.() -> Unit,
     dismissContent: @Composable RowScope.() -> Unit
 ) = WithConstraints(modifier) {
@@ -193,10 +176,14 @@
     if (StartToEnd in directions) anchors += width to DismissedToEnd
     if (EndToStart in directions) anchors += -width to DismissedToStart
 
+    val thresholds = { from: DismissValue, to: DismissValue ->
+        dismissThresholds(getDismissDirection(from, to)!!)
+    }
+
     Stack(Modifier.swipeable(
         state = state,
         anchors = anchors,
-        thresholds = fractionalThresholds(0.25f),
+        thresholds = thresholds,
         orientation = Orientation.Horizontal,
         enabled = state.value == Default,
         reverseDirection = isRtl
@@ -210,4 +197,24 @@
             modifier = Modifier.offsetPx(x = state.offset)
         )
     }
+}
+
+private fun getDismissDirection(from: DismissValue, to: DismissValue): DismissDirection? {
+    return when {
+        // settled at the default state
+        from == to && from == Default -> null
+        // has been dismissed to the end
+        from == to && from == DismissedToEnd -> StartToEnd
+        // has been dismissed to the start
+        from == to && from == DismissedToStart -> EndToStart
+        // is currently being dismissed to the end
+        from == Default && to == DismissedToEnd -> StartToEnd
+        // is currently being dismissed to the start
+        from == Default && to == DismissedToStart -> EndToStart
+        // has been dismissed to the end but is now animated back to default
+        from == DismissedToEnd && to == Default -> StartToEnd
+        // has been dismissed to the start but is now animated back to default
+        from == DismissedToStart && to == Default -> EndToStart
+        else -> null
+    }
 }
\ No newline at end of file
diff --git a/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt b/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
index 256f408..8d5d7ec 100644
--- a/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
+++ b/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
@@ -291,10 +291,10 @@
  * @param T The type of the state.
  * @param state The state of the [swipeable].
  * @param anchors Pairs of anchors and states, used to map anchors to states and vice versa.
- * @param thresholds The thresholds between anchors that determine which anchor to animate to
- * when the user stops swiping, represented as a lambda that takes a pair of anchors and returns
- * a value between them. Note the order of the anchors matters as it indicates the swipe direction.
- * An easy way to define these thresholds is using [fixedThresholds] or [fractionalThresholds].
+ * @param thresholds Specifies where the thresholds between the states are. The thresholds will be
+ * used to determine which state to animate to when swiping stops. This is represented as a lambda
+ * that takes two states and returns the threshold between them in the form of a [ThresholdConfig].
+ * Note that the order of the states corresponds to the swipe direction.
  * @param orientation The orientation in which the [swipeable] can be swiped.
  * @param enabled Whether this [swipeable] is enabled and should react to the user's input.
  * @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom
@@ -308,7 +308,7 @@
 fun <T> Modifier.swipeable(
     state: SwipeableState<T>,
     anchors: Map<Float, T>,
-    thresholds: SwipeableThresholds,
+    thresholds: (from: T, to: T) -> ThresholdConfig,
     orientation: Orientation,
     enabled: Boolean = true,
     reverseDirection: Boolean = false,
@@ -324,7 +324,11 @@
     }
     val density = DensityAmbient.current
     state.anchors = anchors
-    state.thresholds = { a, b -> density.thresholds(a, b) }
+    state.thresholds = { a, b ->
+        val from = anchors.getValue(a)
+        val to = anchors.getValue(b)
+        with(thresholds(from, to)) { density.computeThreshold(a, b) }
+    }
     state.animatedFloat.setBounds(minValue, maxValue)
 
     val lastAnchor = anchors.getOffset(state.value)!!
@@ -343,7 +347,7 @@
         adjustTarget = { target ->
             val adjusted = adjustTarget(
                 anchors = anchors.keys,
-                thresholds = { a, b -> density.thresholds(a, b) },
+                thresholds = state.thresholds,
                 target = target,
                 lastAnchor = lastAnchor
             )
@@ -366,29 +370,46 @@
 }
 
 /**
- * Type alias for the lambda that will be invoked to compute the thresholds between anchors in
- * [Modifier.swipeable]. This takes two anchors, whose the order indicates the swipe direction.
+ * Interface to compute a threshold between two anchors/states in a [swipeable].
+ *
+ * To define a [ThresholdConfig], consider using [FixedThreshold] and [FractionalThreshold].
  */
-typealias SwipeableThresholds = Density.(fromAnchor: Float, toAnchor: Float) -> Float
+@Stable
+@ExperimentalMaterialApi
+interface ThresholdConfig {
+    /**
+     * Compute the value of the threshold (in pixels), once the values of the anchors are known.
+     */
+    fun Density.computeThreshold(fromValue: Float, toValue: Float): Float
+}
 
 /**
- * Constructor for fixed thresholds between anchors.
+ * A fixed threshold will be at an [offset] away from the first anchor.
  *
- * @param offset Each threshold will be at this offset (in dp) away from the first anchor.
+ * @param offset The offset (in dp) that the threshold will be at.
  */
+@Immutable
 @ExperimentalMaterialApi
-fun fixedThresholds(offset: Dp): SwipeableThresholds =
-    { fromAnchor, toAnchor -> fromAnchor + offset.toPx() * sign(toAnchor - fromAnchor) }
+data class FixedThreshold(private val offset: Dp) : ThresholdConfig {
+    override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
+        return fromValue + offset.toPx() * sign(toValue - fromValue)
+    }
+}
 
 /**
- * Constructor for fractional thresholds between anchors.
+ * A fractional threshold will be at a [fraction] of the way between the two anchors.
  *
- * @param fraction Each threshold will be at this fraction of the way between the two anchors.
+ * @param fraction The fraction (between 0 and 1) that the threshold will be at.
  */
+@Immutable
 @ExperimentalMaterialApi
-fun fractionalThresholds(
-    @FloatRange(from = 0.0, to = 1.0) fraction: Float
-): SwipeableThresholds = { fromAnchor, toAnchor -> lerp(fromAnchor, toAnchor, fraction) }
+data class FractionalThreshold(
+    @FloatRange(from = 0.0, to = 1.0) private val fraction: Float
+) : ThresholdConfig {
+    override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
+        return lerp(fromValue, toValue, fraction)
+    }
+}
 
 @Stable
 private class AnimatedFloatByState(
diff --git a/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/Switch.kt b/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
index 94757fd..ad4db4b 100644
--- a/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
+++ b/ui/ui-material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
@@ -83,7 +83,7 @@
             .swipeable(
                 state = swipeableState,
                 anchors = mapOf(minBound to false, maxBound to true),
-                thresholds = fractionalThresholds(0.5f),
+                thresholds = { _, _ -> FractionalThreshold(0.5f) },
                 orientation = Orientation.Horizontal,
                 enabled = enabled,
                 reverseDirection = isRtl,