Merge "Replacing NestedScroll Sources." into androidx-main
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
index 90c2c5d..12c29ee 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
@@ -25,6 +25,7 @@
import android.os.HandlerThread
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
+import androidx.camera.camera2.pipe.compat.AudioRestrictionController
import androidx.camera.camera2.pipe.config.CameraGraphConfigModule
import androidx.camera.camera2.pipe.config.CameraPipeComponent
import androidx.camera.camera2.pipe.config.CameraPipeConfigModule
@@ -116,6 +117,16 @@
}
/**
+ * This gets and sets the global [AudioRestrictionMode] tracked by [AudioRestrictionController].
+ */
+ var globalAudioRestrictionMode: AudioRestrictionMode
+ get(): AudioRestrictionMode =
+ component.cameraAudioRestrictionController().globalAudioRestrictionMode
+ set(value: AudioRestrictionMode) {
+ component.cameraAudioRestrictionController().globalAudioRestrictionMode = value
+ }
+
+ /**
* Application level configuration for [CameraPipe]. Nullable values are optional and reasonable
* defaults will be provided if values are not specified.
*/
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt
index 0d5c122..98dc196 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraPipeComponent.kt
@@ -32,6 +32,8 @@
import androidx.camera.camera2.pipe.CameraPipe.CameraMetadataConfig
import androidx.camera.camera2.pipe.CameraSurfaceManager
import androidx.camera.camera2.pipe.compat.AndroidDevicePolicyManagerWrapper
+import androidx.camera.camera2.pipe.compat.AudioRestrictionController
+import androidx.camera.camera2.pipe.compat.AudioRestrictionControllerImpl
import androidx.camera.camera2.pipe.compat.DevicePolicyManagerWrapper
import androidx.camera.camera2.pipe.core.Debug
import androidx.camera.camera2.pipe.core.SystemTimeSource
@@ -71,6 +73,7 @@
fun cameraGraphComponentBuilder(): CameraGraphComponent.Builder
fun cameras(): CameraDevices
fun cameraSurfaceManager(): CameraSurfaceManager
+ fun cameraAudioRestrictionController(): AudioRestrictionController
}
@Module(includes = [ThreadConfigModule::class], subcomponents = [CameraGraphComponent::class])
@@ -165,5 +168,9 @@
@Singleton
@Provides
fun provideCameraSurfaceManager() = CameraSurfaceManager()
+
+ @Singleton
+ @Provides
+ fun provideAudioRestrictionController() = AudioRestrictionControllerImpl()
}
}
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
index c61b2b4..e64e886 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/ConcurrentCameraActivity.java
@@ -22,7 +22,7 @@
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
-import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.CameraCharacteristics;
import android.os.Build;
import android.os.Bundle;
import android.view.MotionEvent;
@@ -38,8 +38,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatActivity;
-import androidx.camera.camera2.interop.Camera2CameraInfo;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraInfo;
@@ -50,6 +50,7 @@
import androidx.camera.core.MeteringPoint;
import androidx.camera.core.Preview;
import androidx.camera.core.UseCaseGroup;
+import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.app.ActivityCompat;
@@ -90,8 +91,8 @@
private boolean mIsConcurrentModeOn = false;
private boolean mIsLayoutPiP = true;
private boolean mIsFrontPrimary = true;
-
private boolean mIsDualSelfieEnabled = false;
+ private boolean mIsCameraPipeEnabled = false;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -129,6 +130,8 @@
mIsLayoutPiP = true;
bindPreviewForSingle(mCameraProvider);
mIsConcurrentModeOn = false;
+ mIsDualSelfieEnabled = false;
+ mDualSelfieButton.setChecked(false);
} else {
mIsLayoutPiP = true;
bindPreviewForPiP(mCameraProvider);
@@ -170,7 +173,13 @@
}
}
+ @SuppressLint("NullAnnotationGroup")
+ @OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
private void startCamera() {
+ if (mIsCameraPipeEnabled) {
+ ProcessCameraProvider.configureInstance(CameraPipeConfig.defaultConfig());
+ }
+
final ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
ProcessCameraProvider.getInstance(this);
cameraProviderFuture.addListener(() -> {
@@ -265,7 +274,8 @@
}
@SuppressLint("NullAnnotationGroup")
- @OptIn(markerClass = ExperimentalCamera2Interop.class)
+ @OptIn(markerClass = {ExperimentalCamera2Interop.class,
+ androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class})
private void bindToLifecycleForConcurrentCamera(
@NonNull ProcessCameraProvider cameraProvider,
@NonNull LifecycleOwner lifecycleOwner,
@@ -287,13 +297,18 @@
String innerPhysicalCameraId = null;
String outerPhysicalCameraId = null;
for (CameraInfo info : cameraInfoPrimary.getPhysicalCameraInfos()) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- if (Camera2CameraInfo.from(info).getCameraCharacteristic(LENS_POSE_REFERENCE)
- == CameraMetadata.LENS_POSE_REFERENCE_PRIMARY_CAMERA) {
- innerPhysicalCameraId = Camera2CameraInfo.from(info).getCameraId();
- } else {
- outerPhysicalCameraId = Camera2CameraInfo.from(info).getCameraId();
- }
+ if (isPrimaryCamera(info)) {
+ innerPhysicalCameraId = mIsCameraPipeEnabled
+ ? androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo
+ .from(info).getCameraId()
+ : androidx.camera.camera2.interop.Camera2CameraInfo
+ .from(info).getCameraId();
+ } else {
+ outerPhysicalCameraId = mIsCameraPipeEnabled
+ ? androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo
+ .from(info).getCameraId()
+ : androidx.camera.camera2.interop.Camera2CameraInfo
+ .from(info).getCameraId();
}
}
@@ -379,6 +394,24 @@
}
}
+ @SuppressLint("NullAnnotationGroup")
+ @OptIn(markerClass = { ExperimentalCamera2Interop.class,
+ androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class })
+ private boolean isPrimaryCamera(@NonNull CameraInfo info) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ return true;
+ }
+ if (mIsCameraPipeEnabled) {
+ return androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo.from(info)
+ .getCameraCharacteristic(LENS_POSE_REFERENCE)
+ == CameraCharacteristics.LENS_POSE_REFERENCE_PRIMARY_CAMERA;
+ } else {
+ return androidx.camera.camera2.interop.Camera2CameraInfo.from(info)
+ .getCameraCharacteristic(LENS_POSE_REFERENCE)
+ == CameraCharacteristics.LENS_POSE_REFERENCE_PRIMARY_CAMERA;
+ }
+ }
+
private void setupZoomAndTapToFocus(Camera camera, PreviewView previewView) {
ScaleGestureDetector scaleDetector = new ScaleGestureDetector(this,
new ScaleGestureDetector.SimpleOnScaleGestureListener() {
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index d44514f..d359734 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -435,7 +435,7 @@
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface BringIntoViewSpec {
- method public float calculateScrollDistance(float offset, float size, float containerSize);
+ method public default float calculateScrollDistance(float offset, float size, float containerSize);
method public default androidx.compose.animation.core.AnimationSpec<java.lang.Float> getScrollAnimationSpec();
property public default androidx.compose.animation.core.AnimationSpec<java.lang.Float> scrollAnimationSpec;
field public static final androidx.compose.foundation.gestures.BringIntoViewSpec.Companion Companion;
@@ -446,6 +446,11 @@
property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> DefaultScrollAnimationSpec;
}
+ public final class BringIntoViewSpec_androidKt {
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.foundation.gestures.BringIntoViewSpec> getLocalBringIntoViewSpec();
+ property @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.foundation.gestures.BringIntoViewSpec> LocalBringIntoViewSpec;
+ }
+
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface Drag2DScope {
method public void dragBy(long pixels);
}
@@ -544,7 +549,7 @@
}
public final class ScrollableDefaults {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec();
+ method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public androidx.compose.foundation.OverscrollEffect overscrollEffect();
method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
@@ -552,7 +557,7 @@
}
public final class ScrollableKt {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec? bringIntoViewSpec);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 01da236..de43720 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -437,7 +437,7 @@
}
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface BringIntoViewSpec {
- method public float calculateScrollDistance(float offset, float size, float containerSize);
+ method public default float calculateScrollDistance(float offset, float size, float containerSize);
method public default androidx.compose.animation.core.AnimationSpec<java.lang.Float> getScrollAnimationSpec();
property public default androidx.compose.animation.core.AnimationSpec<java.lang.Float> scrollAnimationSpec;
field public static final androidx.compose.foundation.gestures.BringIntoViewSpec.Companion Companion;
@@ -448,6 +448,11 @@
property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> DefaultScrollAnimationSpec;
}
+ public final class BringIntoViewSpec_androidKt {
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.foundation.gestures.BringIntoViewSpec> getLocalBringIntoViewSpec();
+ property @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.foundation.gestures.BringIntoViewSpec> LocalBringIntoViewSpec;
+ }
+
@SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface Drag2DScope {
method public void dragBy(long pixels);
}
@@ -546,7 +551,7 @@
}
public final class ScrollableDefaults {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec();
+ method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec();
method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public androidx.compose.foundation.OverscrollEffect overscrollEffect();
method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
@@ -554,7 +559,7 @@
}
public final class ScrollableKt {
- method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec bringIntoViewSpec);
+ method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.gestures.BringIntoViewSpec? bringIntoViewSpec);
method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier scrollable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.ScrollableState state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional boolean reverseDirection, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
index 2c7ae2c..245b962 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/ListDemos.kt
@@ -18,6 +18,7 @@
package androidx.compose.foundation.demos
import android.annotation.SuppressLint
+import android.content.res.Configuration
import androidx.compose.animation.core.AnimationConstants
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.animateTo
@@ -27,6 +28,7 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.animateScrollBy
@@ -46,8 +48,10 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
@@ -90,7 +94,10 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Color.Companion.Red
+import androidx.compose.ui.graphics.Color.Companion.White
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.Preview
@@ -130,6 +137,7 @@
ComposableDemo("Grid drag and drop") { LazyGridDragAndDropDemo() },
ComposableDemo("Staggered grid") { LazyStaggeredGridDemo() },
ComposableDemo("Animate item placement") { AnimateItemPlacementDemo() },
+ ComposableDemo("Focus Scrolling") { BringIntoViewDemo() },
PagingDemos
)
@@ -349,7 +357,8 @@
Spacer(
Modifier
.fillParentMaxSize()
- .background(it))
+ .background(it)
+ )
}
}
}
@@ -555,6 +564,7 @@
}
}
}
+
val lazyContent: LazyListScope.() -> Unit = {
items(count) {
item1(it)
@@ -1037,9 +1047,11 @@
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun AnimateItemPlacementDemo() {
- val items = remember { mutableStateListOf<Int>().apply {
- repeat(20) { add(it) }
- } }
+ val items = remember {
+ mutableStateListOf<Int>().apply {
+ repeat(20) { add(it) }
+ }
+ }
val selectedIndexes = remember { mutableStateMapOf<Int, Boolean>() }
var reverse by remember { mutableStateOf(false) }
Column {
@@ -1068,7 +1080,8 @@
LazyColumn(
Modifier
.fillMaxWidth()
- .weight(1f), reverseLayout = reverse) {
+ .weight(1f), reverseLayout = reverse
+ ) {
items(items, key = { it }) { item ->
val selected = selectedIndexes.getOrDefault(item, false)
val modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
@@ -1089,7 +1102,8 @@
Spacer(
Modifier
.width(16.dp)
- .height(height))
+ .height(height)
+ )
Text("Item $item")
}
}
@@ -1105,3 +1119,31 @@
})
}
}
+
+@Preview(uiMode = Configuration.UI_MODE_TYPE_TELEVISION)
+@Composable
+private fun BringIntoViewDemo() {
+ LazyRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ ) {
+ items(100) {
+ var color by remember { mutableStateOf(Color.White) }
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .padding(4.dp)
+ .background(Color.Gray)
+ .onFocusChanged {
+ color = if (it.isFocused) Red else White
+ }
+ .border(5.dp, color)
+ .focusable(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(text = it.toString())
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt
index 1645cb76..ae7c7ec 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt
@@ -17,15 +17,26 @@
package androidx.compose.foundation.samples
import androidx.annotation.Sampled
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.BringIntoViewSpec
+import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Icon
@@ -34,15 +45,20 @@
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import kotlin.math.abs
import kotlin.math.roundToInt
@Sampled
@@ -113,3 +129,68 @@
)
}
}
+
+@ExperimentalFoundationApi
+@Sampled
+@Composable
+fun FocusScrollingInLazyRowSample() {
+ // a bring into view spec that pivots around the center of the scrollable container
+ val customBringIntoViewSpec = object : BringIntoViewSpec {
+ val customAnimationSpec = tween<Float>(easing = LinearEasing)
+ override val scrollAnimationSpec: AnimationSpec<Float>
+ get() = customAnimationSpec
+
+ override fun calculateScrollDistance(
+ offset: Float,
+ size: Float,
+ containerSize: Float
+ ): Float {
+ val trailingEdgeOfItemRequestingFocus = offset + size
+
+ val sizeOfItemRequestingFocus =
+ abs(trailingEdgeOfItemRequestingFocus - offset)
+ val childSmallerThanParent = sizeOfItemRequestingFocus <= containerSize
+ val initialTargetForLeadingEdge =
+ containerSize / 2f - (sizeOfItemRequestingFocus / 2f)
+ val spaceAvailableToShowItem = containerSize - initialTargetForLeadingEdge
+
+ val targetForLeadingEdge =
+ if (childSmallerThanParent &&
+ spaceAvailableToShowItem < sizeOfItemRequestingFocus
+ ) {
+ containerSize - sizeOfItemRequestingFocus
+ } else {
+ initialTargetForLeadingEdge
+ }
+
+ return offset - targetForLeadingEdge
+ }
+ }
+
+ // LocalBringIntoViewSpec will apply to all scrollables in the hierarchy.
+ CompositionLocalProvider(LocalBringIntoViewSpec provides customBringIntoViewSpec) {
+ LazyRow(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ ) {
+ items(100) {
+ var color by remember { mutableStateOf(Color.White) }
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .padding(4.dp)
+ .background(Color.Gray)
+ .onFocusChanged {
+ color = if (it.isFocused) Color.Red else Color.White
+ }
+ .border(5.dp, color)
+ .focusable(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(text = it.toString())
+ }
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.android.kt
new file mode 100644
index 0000000..482a642
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.android.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.foundation.gestures
+
+import android.content.pm.PackageManager.FEATURE_LEANBACK
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.BringIntoViewSpec.Companion.DefaultBringIntoViewSpec
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.compositionLocalWithComputedDefaultOf
+import androidx.compose.ui.platform.LocalContext
+import kotlin.math.abs
+
+/**
+ * A composition local to customize the focus scrolling behavior used by some scrollable containers.
+ * [LocalBringIntoViewSpec] has a platform defined behavior. If the App is running on a TV device,
+ * the scroll behavior will pivot around 30% of the container size. For other platforms, the scroll
+ * behavior will move the least to bring the requested region into view.
+ */
+@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:ExperimentalFoundationApi
+@ExperimentalFoundationApi
+actual val LocalBringIntoViewSpec: ProvidableCompositionLocal<BringIntoViewSpec> =
+ compositionLocalWithComputedDefaultOf {
+ val hasTvFeature =
+ LocalContext.currentValue.packageManager.hasSystemFeature(FEATURE_LEANBACK)
+ if (!hasTvFeature) {
+ DefaultBringIntoViewSpec
+ } else {
+ PivotBringIntoViewSpec
+ }
+ }
+
+@OptIn(ExperimentalFoundationApi::class)
+internal val PivotBringIntoViewSpec = object : BringIntoViewSpec {
+ val parentFraction = 0.3f
+ val childFraction = 0f
+ override val scrollAnimationSpec: AnimationSpec<Float> = tween<Float>(
+ durationMillis = 125,
+ easing = CubicBezierEasing(0.25f, 0.1f, .25f, 1f)
+ )
+
+ override fun calculateScrollDistance(
+ offset: Float,
+ size: Float,
+ containerSize: Float
+ ): Float {
+ val leadingEdgeOfItemRequestingFocus = offset
+ val trailingEdgeOfItemRequestingFocus = offset + size
+
+ val sizeOfItemRequestingFocus =
+ abs(trailingEdgeOfItemRequestingFocus - leadingEdgeOfItemRequestingFocus)
+ val childSmallerThanParent = sizeOfItemRequestingFocus <= containerSize
+ val initialTargetForLeadingEdge =
+ parentFraction * containerSize -
+ (childFraction * sizeOfItemRequestingFocus)
+ val spaceAvailableToShowItem = containerSize - initialTargetForLeadingEdge
+
+ val targetForLeadingEdge =
+ if (childSmallerThanParent &&
+ spaceAvailableToShowItem < sizeOfItemRequestingFocus
+ ) {
+ containerSize - sizeOfItemRequestingFocus
+ } else {
+ initialTargetForLeadingEdge
+ }
+
+ return leadingEdgeOfItemRequestingFocus - targetForLeadingEdge
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ScrollingContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ScrollingContainer.kt
index 0504ecc..244dc7a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ScrollingContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ScrollingContainer.kt
@@ -37,9 +37,10 @@
reverseScrolling: Boolean,
flingBehavior: FlingBehavior?,
interactionSource: MutableInteractionSource?,
- bringIntoViewSpec: BringIntoViewSpec = ScrollableDefaults.bringIntoViewSpec()
+ bringIntoViewSpec: BringIntoViewSpec? = null
): Modifier {
val overscrollEffect = ScrollableDefaults.overscrollEffect()
+
return clipScrollableContainer(orientation)
.overscroll(overscrollEffect)
.scrollable(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.kt
new file mode 100644
index 0000000..f030e6a
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.foundation.gestures
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import kotlin.math.abs
+
+/**
+ * A composition local to customize the focus scrolling behavior used by some scrollable containers.
+ * [LocalBringIntoViewSpec] has a platform defined default behavior.
+ */
+@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:ExperimentalFoundationApi
+@ExperimentalFoundationApi
+expect val LocalBringIntoViewSpec: ProvidableCompositionLocal<BringIntoViewSpec>
+
+/**
+ * The configuration of how a scrollable reacts to bring into view requests.
+ *
+ * Note: API shape and naming are still being refined, therefore API is marked as experimental.
+ *
+ * Check the following sample for a use case usage of this API:
+ * @sample androidx.compose.foundation.samples.FocusScrollingInLazyRowSample
+ */
+@ExperimentalFoundationApi
+@Stable
+interface BringIntoViewSpec {
+
+ /**
+ * An Animation Spec to be used as the animation to run to fulfill the BringIntoView requests.
+ */
+ val scrollAnimationSpec: AnimationSpec<Float> get() = DefaultScrollAnimationSpec
+
+ /**
+ * Calculate the offset needed to bring one of the scrollable container's child into view.
+ * This will be called for every frame of the scrolling animation. This means that, as the
+ * animation progresses, the offset will naturally change to fulfill the scroll request.
+ *
+ * All distances below are represented in pixels.
+ * @param offset from the side closest to the start of the container.
+ * @param size is the child size.
+ * @param containerSize Is the main axis size of the scrollable container.
+ *
+ * @return The necessary amount to scroll to satisfy the bring into view request.
+ * Returning zero from here means that the request was satisfied and the scrolling animation
+ * should stop.
+ */
+ fun calculateScrollDistance(
+ offset: Float,
+ size: Float,
+ containerSize: Float
+ ): Float = defaultCalculateScrollDistance(offset, size, containerSize)
+
+ companion object {
+
+ /**
+ * The default animation spec used by [Modifier.scrollable] to run Bring Into View requests.
+ */
+ val DefaultScrollAnimationSpec: AnimationSpec<Float> = spring()
+
+ internal val DefaultBringIntoViewSpec = object : BringIntoViewSpec {}
+
+ internal fun defaultCalculateScrollDistance(
+ offset: Float,
+ size: Float,
+ containerSize: Float
+ ): Float {
+ val trailingEdge = offset + size
+ @Suppress("UnnecessaryVariable") val leadingEdge = offset
+ return when {
+
+ // If the item is already visible, no need to scroll.
+ leadingEdge >= 0 && trailingEdge <= containerSize -> 0f
+
+ // If the item is visible but larger than the parent, we don't scroll.
+ leadingEdge < 0 && trailingEdge > containerSize -> 0f
+
+ // Find the minimum scroll needed to make one of the edges coincide with the parent's
+ // edge.
+ abs(leadingEdge) < abs(trailingEdge - containerSize) -> leadingEdge
+ else -> trailingEdge - containerSize
+ }
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
index 9976dc1..18ce61c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewNode.kt
@@ -26,7 +26,9 @@
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.LayoutAwareModifierNode
+import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.node.requireLayoutCoordinates
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toSize
@@ -65,8 +67,9 @@
private var orientation: Orientation,
private var scrollState: ScrollableState,
private var reverseDirection: Boolean,
- private var bringIntoViewSpec: BringIntoViewSpec
-) : Modifier.Node(), BringIntoViewResponder, LayoutAwareModifierNode {
+ private var bringIntoViewSpec: BringIntoViewSpec?
+) : Modifier.Node(), BringIntoViewResponder, LayoutAwareModifierNode,
+ CompositionLocalConsumerModifierNode {
/**
* Ongoing requests from [bringChildIntoView], with the invariant that it is always sorted by
@@ -111,6 +114,10 @@
return computeDestination(localRect, viewportSize)
}
+ private fun requireBringIntoViewSpec(): BringIntoViewSpec {
+ return bringIntoViewSpec ?: currentValueOf(LocalBringIntoViewSpec)
+ }
+
override suspend fun bringChildIntoView(localRect: () -> Rect?) {
// Avoid creating no-op requests and no-op animations if the request does not require
// scrolling or returns null.
@@ -171,6 +178,7 @@
}
private fun launchAnimation() {
+ val bringIntoViewSpec = requireBringIntoViewSpec()
check(!isAnimationRunning) { "launchAnimation called when previous animation was running" }
if (DEBUG) println("[$TAG] launchAnimation")
@@ -182,7 +190,7 @@
try {
isAnimationRunning = true
scrollState.scroll {
- animationState.value = calculateScrollDelta()
+ animationState.value = calculateScrollDelta(bringIntoViewSpec)
if (DEBUG) println(
"[$TAG] Starting scroll animation down from ${animationState.value}…"
)
@@ -247,7 +255,7 @@
// Compute a new scroll target taking into account any resizes,
// replacements, or added/removed requests since the last frame.
- animationState.value = calculateScrollDelta()
+ animationState.value = calculateScrollDelta(bringIntoViewSpec)
if (DEBUG) println(
"[$TAG] scroll target after frame: ${animationState.value}"
)
@@ -286,7 +294,7 @@
* Calculates how far we need to scroll to satisfy all existing BringIntoView requests and the
* focused child tracking.
*/
- private fun calculateScrollDelta(): Float {
+ private fun calculateScrollDelta(bringIntoViewSpec: BringIntoViewSpec): Float {
if (viewportSize == IntSize.Zero) return 0f
val rectangleToMakeVisible: Rect = findBringIntoViewRequest()
@@ -358,7 +366,7 @@
return when (orientation) {
Vertical -> Offset(
x = 0f,
- y = bringIntoViewSpec.calculateScrollDistance(
+ y = requireBringIntoViewSpec().calculateScrollDistance(
childBounds.top,
childBounds.bottom - childBounds.top,
size.height
@@ -366,7 +374,7 @@
)
Horizontal -> Offset(
- x = bringIntoViewSpec.calculateScrollDistance(
+ x = requireBringIntoViewSpec().calculateScrollDistance(
childBounds.left,
childBounds.right - childBounds.left,
size.width
@@ -390,7 +398,7 @@
orientation: Orientation,
state: ScrollableState,
reverseDirection: Boolean,
- bringIntoViewSpec: BringIntoViewSpec
+ bringIntoViewSpec: BringIntoViewSpec?
) {
this.orientation = orientation
this.scrollState = state
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 72c2b20..14d7aee 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -16,11 +16,9 @@
package androidx.compose.foundation.gestures
-import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.animation.core.animateDecay
-import androidx.compose.animation.core.spring
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.animation.splineBasedDecay
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -154,7 +152,9 @@
* @param interactionSource [MutableInteractionSource] that will be used to emit
* drag events when this scrollable is being dragged.
* @param bringIntoViewSpec The configuration that this scrollable should use to perform
- * scrolling when scroll requests are received from the focus system.
+ * scrolling when scroll requests are received from the focus system. If null is provided the
+ * system will use the behavior provided by [LocalBringIntoViewSpec] which by default has a
+ * platform dependent implementation.
*
* Note: This API is experimental as it brings support for some experimental features:
* [overscrollEffect] and [bringIntoViewSpec].
@@ -169,7 +169,7 @@
reverseDirection: Boolean = false,
flingBehavior: FlingBehavior? = null,
interactionSource: MutableInteractionSource? = null,
- bringIntoViewSpec: BringIntoViewSpec = ScrollableDefaults.bringIntoViewSpec()
+ bringIntoViewSpec: BringIntoViewSpec? = null
) = this then ScrollableElement(
state,
orientation,
@@ -190,7 +190,7 @@
val reverseDirection: Boolean,
val flingBehavior: FlingBehavior?,
val interactionSource: MutableInteractionSource?,
- val bringIntoViewSpec: BringIntoViewSpec
+ val bringIntoViewSpec: BringIntoViewSpec?
) : ModifierNodeElement<ScrollableNode>() {
override fun create(): ScrollableNode {
return ScrollableNode(
@@ -269,7 +269,7 @@
enabled: Boolean,
reverseDirection: Boolean,
interactionSource: MutableInteractionSource?,
- bringIntoViewSpec: BringIntoViewSpec
+ private val bringIntoViewSpec: BringIntoViewSpec?
) : DragGestureNode(
canDrag = CanDragCalculation,
enabled = enabled,
@@ -354,7 +354,7 @@
reverseDirection: Boolean,
flingBehavior: FlingBehavior?,
interactionSource: MutableInteractionSource?,
- bringIntoViewSpec: BringIntoViewSpec
+ bringIntoViewSpec: BringIntoViewSpec?
) {
if (this.enabled != enabled) { // enabled changed
@@ -490,80 +490,6 @@
}
/**
- * The configuration of how a scrollable reacts to bring into view requests.
- *
- * Note: API shape and naming are still being refined, therefore API is marked as experimental.
- */
-@ExperimentalFoundationApi
-@Stable
-interface BringIntoViewSpec {
-
- /**
- * A retargetable Animation Spec to be used as the animation to run to fulfill the
- * BringIntoView requests.
- */
- val scrollAnimationSpec: AnimationSpec<Float> get() = DefaultScrollAnimationSpec
-
- /**
- * Calculate the offset needed to bring one of the scrollable container's child into view.
- *
- * @param offset from the side closest to the origin (For the x-axis this is 'left',
- * for the y-axis this is 'top').
- * @param size is the child size.
- * @param containerSize Is the main axis size of the scrollable container.
- *
- * All distances above are represented in pixels.
- *
- * @return The necessary amount to scroll to satisfy the bring into view request.
- * Returning zero from here means that the request was satisfied and the scrolling animation
- * should stop.
- *
- * This will be called for every frame of the scrolling animation. This means that, as the
- * animation progresses, the offset will naturally change to fulfill the scroll request.
- */
- fun calculateScrollDistance(
- offset: Float,
- size: Float,
- containerSize: Float
- ): Float
-
- companion object {
-
- /**
- * The default animation spec used by [Modifier.scrollable] to run Bring Into View requests.
- */
- val DefaultScrollAnimationSpec: AnimationSpec<Float> = spring()
-
- internal val DefaultBringIntoViewSpec = object : BringIntoViewSpec {
-
- override val scrollAnimationSpec: AnimationSpec<Float> = DefaultScrollAnimationSpec
-
- override fun calculateScrollDistance(
- offset: Float,
- size: Float,
- containerSize: Float
- ): Float {
- val trailingEdge = offset + size
- @Suppress("UnnecessaryVariable") val leadingEdge = offset
- return when {
-
- // If the item is already visible, no need to scroll.
- leadingEdge >= 0 && trailingEdge <= containerSize -> 0f
-
- // If the item is visible but larger than the parent, we don't scroll.
- leadingEdge < 0 && trailingEdge > containerSize -> 0f
-
- // Find the minimum scroll needed to make one of the edges coincide with the parent's
- // edge.
- abs(leadingEdge) < abs(trailingEdge - containerSize) -> leadingEdge
- else -> trailingEdge - containerSize
- }
- }
- }
- }
-}
-
-/**
* Contains the default values used by [scrollable]
*/
object ScrollableDefaults {
@@ -619,6 +545,13 @@
* A default implementation for [BringIntoViewSpec] that brings a child into view
* using the least amount of effort.
*/
+ @Deprecated(
+ "This has been replaced by composition locals LocalBringIntoViewSpec",
+ replaceWith = ReplaceWith(
+ "LocalBringIntoView.current",
+ "androidx.compose.foundation.gestures.LocalBringIntoViewSpec"
+ )
+ )
@ExperimentalFoundationApi
fun bringIntoViewSpec(): BringIntoViewSpec = DefaultBringIntoViewSpec
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
index 0de86a8..8771eed 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
@@ -22,9 +22,9 @@
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.BringIntoViewSpec
import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.TargetedFlingBehavior
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
@@ -134,7 +134,7 @@
PagerWrapperFlingBehavior(flingBehavior, state)
}
- val defaultBringIntoViewSpec = ScrollableDefaults.bringIntoViewSpec()
+ val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
val pagerBringIntoViewSpec = remember(state, defaultBringIntoViewSpec) {
PagerBringIntoViewSpec(
state,
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.desktop.kt
new file mode 100644
index 0000000..061d648
--- /dev/null
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/gestures/BringIntoViewSpec.desktop.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.foundation.gestures
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.staticCompositionLocalOf
+
+/*
+* A composition local to customize the focus scrolling behavior used by some scrollable containers.
+* [LocalBringIntoViewSpec] has a platform defined behavior. The scroll default behavior will move
+* the least to bring the requested region into view.
+*/
+@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+@get:ExperimentalFoundationApi
+@ExperimentalFoundationApi
+actual val LocalBringIntoViewSpec: ProvidableCompositionLocal<BringIntoViewSpec> =
+ staticCompositionLocalOf {
+ BringIntoViewSpec.DefaultBringIntoViewSpec
+ }
diff --git a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt
index 811c9f7..0da1391 100644
--- a/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt
+++ b/datastore/datastore-core/src/androidInstrumentedTest/kotlin/androidx/datastore/core/multiprocess/MultiProcessDataStoreIpcTest.kt
@@ -30,7 +30,6 @@
import androidx.datastore.core.twoWayIpc.IpcUnit
import androidx.datastore.core.twoWayIpc.TwoWayIpcSubject
import androidx.datastore.testing.TestMessageProto.FooProto
-import androidx.kruth.assertThrows
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
@@ -565,12 +564,11 @@
delay(100)
// process A (could be another thread than 1.) waits to hold file lock 2 (still held by B)
- assertThrows<IOException> {
+ val localUpdate2 = async {
datastore2.updateData {
it.toBuilder().setInteger(4).build()
}
- }.hasMessageThat()
- .contains("Resource deadlock would occur")
+ }
blockWrite.complete(Unit)
commitWriteLatch1.complete(subject2, IpcUnit)
@@ -579,9 +577,11 @@
setTextAction1.await()
setTextAction2.await()
localUpdate1.await()
+ localUpdate2.await()
assertThat(datastore1.data.first().text).isEqualTo("remoteValue")
assertThat(datastore1.data.first().integer).isEqualTo(3)
assertThat(datastore2.data.first().text).isEqualTo("remoteValue")
+ assertThat(datastore2.data.first().integer).isEqualTo(4)
}
}
diff --git a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt
index 3d3edff..e596d66 100644
--- a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt
+++ b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessCoordinator.android.kt
@@ -25,6 +25,7 @@
import java.nio.channels.FileLock
import kotlin.contracts.ExperimentalContracts
import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -43,7 +44,7 @@
FileOutputStream(lockFile).use { lockFileStream ->
var lock: FileLock? = null
try {
- lock = lockFileStream.getChannel().lock(0L, Long.MAX_VALUE, /* shared= */ false)
+ lock = getExclusiveFileLockWithRetryIfDeadlock(lockFileStream)
return block()
} finally {
lock?.release()
@@ -78,7 +79,8 @@
// will throw an IOException with EAGAIN error, instead of returning null as
// specified in {@link FileChannel#tryLock}. We only continue if the error
// message is EAGAIN, otherwise just throw it.
- if (ex.message?.startsWith(LOCK_ERROR_MESSAGE) != true) {
+ if ((ex.message?.startsWith(LOCK_ERROR_MESSAGE) != true) &&
+ (ex.message?.startsWith(DEADLOCK_ERROR_MESSAGE) != true)) {
throw ex
}
}
@@ -162,6 +164,32 @@
}
}
}
+
+ companion object {
+ // Retry with exponential backoff to get file lock if it hits "Resource deadlock would
+ // occur" error until the backoff reaches [MAX_WAIT_MILLIS].
+ private suspend fun getExclusiveFileLockWithRetryIfDeadlock(
+ lockFileStream: FileOutputStream
+ ): FileLock {
+ var backoff = INITIAL_WAIT_MILLIS
+ while (backoff <= MAX_WAIT_MILLIS) {
+ try {
+ return lockFileStream.getChannel().lock(0L, Long.MAX_VALUE, /* shared= */ false)
+ } catch (ex: IOException) {
+ if (ex.message?.contains(DEADLOCK_ERROR_MESSAGE) != true) {
+ throw ex
+ }
+ delay(backoff)
+ backoff *= 2
+ }
+ }
+ return lockFileStream.getChannel().lock(0L, Long.MAX_VALUE, /* shared= */ false)
+ }
+
+ private val DEADLOCK_ERROR_MESSAGE = "Resource deadlock would occur"
+ private val INITIAL_WAIT_MILLIS: Long = 10
+ private val MAX_WAIT_MILLIS: Long = 60000
+ }
}
/**