Merge "[Carousel] Add maskClip and maskBorder modifiers to CarouselItemScope" into androidx-main
diff --git a/compose/material3/material3/api/1.3.0-beta01.txt b/compose/material3/material3/api/1.3.0-beta01.txt
index ca9302d..4f91e60 100644
--- a/compose/material3/material3/api/1.3.0-beta01.txt
+++ b/compose/material3/material3/api/1.3.0-beta01.txt
@@ -2168,6 +2168,9 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselItemScope {
method public androidx.compose.material3.carousel.CarouselItemInfo getCarouselItemInfo();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskBorder(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, androidx.compose.ui.graphics.Shape shape);
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskClip(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Shape shape);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.shape.GenericShape rememberMaskShape(androidx.compose.ui.graphics.Shape shape);
property public abstract androidx.compose.material3.carousel.CarouselItemInfo carouselItemInfo;
}
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index ca9302d..4f91e60 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -2168,6 +2168,9 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselItemScope {
method public androidx.compose.material3.carousel.CarouselItemInfo getCarouselItemInfo();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskBorder(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, androidx.compose.ui.graphics.Shape shape);
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskClip(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Shape shape);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.shape.GenericShape rememberMaskShape(androidx.compose.ui.graphics.Shape shape);
property public abstract androidx.compose.material3.carousel.CarouselItemInfo carouselItemInfo;
}
diff --git a/compose/material3/material3/api/restricted_1.3.0-beta01.txt b/compose/material3/material3/api/restricted_1.3.0-beta01.txt
index ca9302d..4f91e60 100644
--- a/compose/material3/material3/api/restricted_1.3.0-beta01.txt
+++ b/compose/material3/material3/api/restricted_1.3.0-beta01.txt
@@ -2168,6 +2168,9 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselItemScope {
method public androidx.compose.material3.carousel.CarouselItemInfo getCarouselItemInfo();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskBorder(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, androidx.compose.ui.graphics.Shape shape);
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskClip(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Shape shape);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.shape.GenericShape rememberMaskShape(androidx.compose.ui.graphics.Shape shape);
property public abstract androidx.compose.material3.carousel.CarouselItemInfo carouselItemInfo;
}
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index ca9302d..4f91e60 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -2168,6 +2168,9 @@
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public sealed interface CarouselItemScope {
method public androidx.compose.material3.carousel.CarouselItemInfo getCarouselItemInfo();
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskBorder(androidx.compose.ui.Modifier, androidx.compose.foundation.BorderStroke border, androidx.compose.ui.graphics.Shape shape);
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.Modifier maskClip(androidx.compose.ui.Modifier, androidx.compose.ui.graphics.Shape shape);
+ method @androidx.compose.runtime.Composable public androidx.compose.foundation.shape.GenericShape rememberMaskShape(androidx.compose.ui.graphics.Shape shape);
property public abstract androidx.compose.material3.carousel.CarouselItemInfo carouselItemInfo;
}
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
index cf25fa1..f142563 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/CarouselSamples.kt
@@ -19,26 +19,50 @@
import androidx.annotation.DrawableRes
import androidx.annotation.Sampled
import androidx.annotation.StringRes
+import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
-import androidx.compose.material3.Card
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Image
+import androidx.compose.material3.AssistChipDefaults
+import androidx.compose.material3.ElevatedAssistChip
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel
import androidx.compose.material3.carousel.HorizontalUncontainedCarousel
import androidx.compose.material3.carousel.rememberCarouselState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.RoundRect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.lerp
+import kotlin.math.max
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@@ -70,16 +94,12 @@
contentPadding = PaddingValues(horizontal = 16.dp)
) { i ->
val item = items[i]
- Card(
- modifier = Modifier.height(205.dp)
- ) {
- Image(
- painter = painterResource(id = item.imageResId),
- contentDescription = stringResource(item.contentDescriptionResId),
- modifier = Modifier.fillMaxSize(),
- contentScale = ContentScale.Crop
- )
- }
+ Image(
+ modifier = Modifier.height(205.dp).maskClip(MaterialTheme.shapes.extraLarge),
+ painter = painterResource(id = item.imageResId),
+ contentDescription = stringResource(item.contentDescriptionResId),
+ contentScale = ContentScale.Crop
+ )
}
}
@@ -112,17 +132,12 @@
contentPadding = PaddingValues(horizontal = 16.dp)
) { i ->
val item = items[i]
- Card(
- modifier = Modifier
- .height(205.dp)
- ) {
- Image(
- painter = painterResource(id = item.imageResId),
- contentDescription = stringResource(item.contentDescriptionResId),
- modifier = Modifier.fillMaxSize(),
- contentScale = ContentScale.Crop
- )
- }
+ Image(
+ modifier = Modifier.height(205.dp).maskClip(MaterialTheme.shapes.extraLarge),
+ painter = painterResource(id = item.imageResId),
+ contentDescription = stringResource(item.contentDescriptionResId),
+ contentScale = ContentScale.Crop
+ )
}
}
@@ -151,20 +166,67 @@
modifier = Modifier
.width(412.dp)
.height(221.dp),
- preferredItemWidth = 130.dp,
+ preferredItemWidth = 186.dp,
itemSpacing = 8.dp,
contentPadding = PaddingValues(horizontal = 16.dp)
) { i ->
val item = items[i]
- Card(
- modifier = Modifier
- .height(205.dp)
- ) {
+ // For item 1 and 4, create a stacked item layout that clips two images independently
+ // to the item's mask
+ if (i == 1 || i == 4) {
+ Column(
+ modifier = Modifier.height(205.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Image(
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight(.5f)
+ .maskClip(MaterialTheme.shapes.extraLarge)
+ .maskBorder(
+ BorderStroke(3.dp, Color.Magenta),
+ MaterialTheme.shapes.extraLarge
+ ),
+ painter = painterResource(id = item.imageResId),
+ contentDescription = stringResource(item.contentDescriptionResId),
+ contentScale = ContentScale.Crop
+ )
+ Image(
+ modifier = Modifier
+ .fillMaxSize()
+ .maskClip(RoundedCornerShape(8.dp))
+ .maskBorder(
+ BorderStroke(5.dp, Color.Green),
+ RoundedCornerShape(8.dp)
+ ),
+ painter = painterResource(id = item.imageResId),
+ contentDescription = stringResource(item.contentDescriptionResId),
+ contentScale = ContentScale.Crop
+ )
+ }
+ } else {
+ // Mask using a generic path shape
+ val pathShape = remember {
+ object : Shape {
+ override fun createOutline(
+ size: Size,
+ layoutDirection: LayoutDirection,
+ density: Density
+ ): Outline {
+ val roundRect =
+ RoundRect(0f, 0f, size.width, size.height, CornerRadius(30f))
+ val shapePath = Path().apply {
+ addRoundRect(roundRect)
+ }
+ return Outline.Generic(shapePath)
+ }
+ }
+ }
Box(
modifier = Modifier
- .graphicsLayer {
- alpha = carouselItemInfo.size / carouselItemInfo.maxSize
- }
+ .height(205.dp)
+ .maskClip(pathShape)
+ .maskBorder(BorderStroke(5.dp, Color.Red), pathShape),
) {
Image(
painter = painterResource(id = item.imageResId),
@@ -172,10 +234,29 @@
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
- Text(
- text = "sample text",
+ ElevatedAssistChip(
+ onClick = { /* Do something! */ },
+ label = { Text("Image $i") },
modifier = Modifier.graphicsLayer {
- translationX = carouselItemInfo.maskRect.left
+ // Fade the chip in once the carousel item's size is large enough to
+ // display the entire chip
+ alpha = lerp(
+ 0f,
+ 1f,
+ max(
+ size.width - (carouselItemInfo.maxSize) + carouselItemInfo.size,
+ 0f
+ ) / size.width
+ )
+ // Translate the chip to be pinned to the left side of the item's mask
+ translationX = carouselItemInfo.maskRect.left + 8.dp.toPx()
+ },
+ leadingIcon = {
+ Icon(
+ Icons.Filled.Image,
+ contentDescription = "Localized description",
+ Modifier.size(AssistChipDefaults.IconSize)
+ )
}
)
}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselItemScopeTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselItemScopeTest.kt
new file mode 100644
index 0000000..26faa0f
--- /dev/null
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselItemScopeTest.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3.carousel
+
+import android.os.Build
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.GenericShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.GOLDEN_MATERIAL3
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.material3.setMaterialContent
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpRect
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+@OptIn(ExperimentalMaterial3Api::class)
+class CarouselItemScopeTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @get:Rule
+ val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL3)
+
+ val testTag = "ItemTag"
+
+ @Test
+ fun mask_fullyUnmaskedShouldMatchSize() {
+ rule.setMaterialContent(lightColorScheme()) {
+ val scope = createCarouselItemScope(
+ size = 100.dp,
+ minSize = 10.dp,
+ maxSize = 100.dp,
+ maskRect = DpRect(0.dp, 0.dp, 100.dp, 100.dp)
+ )
+ with(scope) {
+ Box(modifier = Modifier
+ .testTag(testTag)
+ .size(100.dp)
+ .maskClip(shape = RoundedCornerShape(28.dp))
+ .background(Color.Red)
+ )
+ }
+ }
+
+ assertCarouselAgainstGolden("mask_fullyUnmaskedShouldMatchSize")
+ }
+
+ @Test
+ fun mask_halfMaksedShouldIntersectSize() {
+ rule.setMaterialContent(lightColorScheme()) {
+ val scope = createCarouselItemScope(
+ size = 50.dp,
+ minSize = 10.dp,
+ maxSize = 100.dp,
+ maskRect = DpRect(25.dp, 25.dp, 75.dp, 75.dp)
+ )
+ with(scope) {
+ Box(modifier = Modifier
+ .testTag(testTag)
+ .size(100.dp)
+ .maskClip(shape = RoundedCornerShape(10.dp))
+ .background(Color.Red)
+ )
+ }
+ }
+
+ assertCarouselAgainstGolden("mask_halfMaskedShouldIntersectSize")
+ }
+
+ @Test
+ fun mask_genericMaskedPathShouldIntersectSize() {
+ val ovalPathShape = GenericShape { size, _ ->
+ addOval(Rect(0f, 0f, size.width, size.height))
+ }
+ rule.setMaterialContent(lightColorScheme()) {
+ val scope = createCarouselItemScope(
+ size = 50.dp,
+ minSize = 10.dp,
+ maxSize = 100.dp,
+ maskRect = DpRect(25.dp, 0.dp, 75.dp, 100.dp)
+ )
+ with(scope) {
+ Box(modifier = Modifier
+ .testTag(testTag)
+ .size(100.dp)
+ .maskClip(shape = ovalPathShape)
+ .background(Color.Red)
+ )
+ }
+ }
+
+ assertCarouselAgainstGolden("mask_genericMaskedPathShouldIntersectSize")
+ }
+
+ @Test
+ fun mask_squareMaskShouldIntersectSize() {
+ rule.setMaterialContent(lightColorScheme()) {
+ val scope = createCarouselItemScope(
+ size = 50.dp,
+ minSize = 10.dp,
+ maxSize = 100.dp,
+ maskRect = DpRect(25.dp, 0.dp, 75.dp, 100.dp)
+ )
+ with(scope) {
+ Box(modifier = Modifier
+ .testTag(testTag)
+ .size(100.dp)
+ .maskClip(shape = RoundedCornerShape(0.dp))
+ .background(Color.Red)
+ )
+ }
+ }
+
+ assertCarouselAgainstGolden("mask_squareMaskShouldIntersectSize")
+ }
+
+ @Test
+ fun maskBorder_fullyUnmaskedShouldMatchSize() {
+ rule.setMaterialContent(lightColorScheme()) {
+ val scope = createCarouselItemScope(
+ size = 100.dp,
+ minSize = 10.dp,
+ maxSize = 100.dp,
+ maskRect = DpRect(0.dp, 0.dp, 100.dp, 100.dp)
+ )
+ with(scope) {
+ Box(modifier = Modifier
+ .testTag(testTag)
+ .size(100.dp)
+ .maskClip(shape = RoundedCornerShape(10.dp))
+ .maskBorder(
+ border = BorderStroke(5.dp, Color.Blue),
+ shape = RoundedCornerShape(10.dp)
+ )
+ .background(Color.Red)
+ )
+ }
+ }
+
+ assertCarouselAgainstGolden("maskBorder_fullyUnmaskedShouldMatchSize")
+ }
+
+ @Test
+ fun maskBorder_triangleMaskShouldIntersectSize() {
+ val triangle = GenericShape { size, _ ->
+ moveTo(size.width / 2f, 0f)
+ lineTo(size.width, size.height)
+ lineTo(0f, size.height)
+ close()
+ }
+ rule.setMaterialContent(lightColorScheme()) {
+ val scope = createCarouselItemScope(
+ size = 100.dp,
+ minSize = 10.dp,
+ maxSize = 100.dp,
+ maskRect = DpRect(25.dp, 25.dp, 75.dp, 75.dp)
+ )
+ with(scope) {
+ Box(modifier = Modifier
+ .testTag(testTag)
+ .size(100.dp)
+ .maskClip(shape = triangle)
+ .maskBorder(
+ border = BorderStroke(5.dp, Color.Blue),
+ shape = triangle
+ )
+ .background(Color.Red)
+ )
+ }
+ }
+
+ assertCarouselAgainstGolden("maskBorder_triangleMaskShouldIntersectSize")
+ }
+
+ private fun createCarouselItemScope(
+ size: Dp,
+ minSize: Dp,
+ maxSize: Dp,
+ maskRect: DpRect
+ ): CarouselItemScope {
+ return CarouselItemScopeImpl(CarouselItemInfoImpl().apply {
+
+ with(rule.density) {
+ sizeState = size.toPx()
+ minSizeState = minSize.toPx()
+ maxSizeState = maxSize.toPx()
+ maskRectState = maskRect.toRect()
+ }
+ })
+ }
+
+ private fun assertCarouselAgainstGolden(goldenIdentifier: String) {
+ rule
+ .onNodeWithTag(testTag)
+ .captureToImage()
+ .assertAgainstGolden(
+ screenshotRule,
+ "carousel_$goldenIdentifier"
+ )
+ }
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
index dfbc26f..2a6e41e 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
@@ -35,18 +35,15 @@
import androidx.compose.foundation.pager.PagerDefaults
import androidx.compose.foundation.pager.PagerSnapDistance
import androidx.compose.foundation.pager.VerticalPager
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ShapeDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Shape
@@ -262,6 +259,17 @@
) { page ->
val carouselItemInfo = remember { CarouselItemInfoImpl() }
val scope = remember { CarouselItemScopeImpl(itemInfo = carouselItemInfo) }
+ val clipShape = remember {
+ object : Shape {
+ override fun createOutline(
+ size: Size,
+ layoutDirection: LayoutDirection,
+ density: Density
+ ): Outline {
+ return Outline.Rectangle(carouselItemInfo.maskRect)
+ }
+ }
+ }
Box(
modifier = Modifier.carouselItem(
@@ -269,6 +277,7 @@
state = state,
strategy = { pageSize.strategy },
carouselItemInfo = carouselItemInfo,
+ clipShape = clipShape
)
) {
scope.content(page)
@@ -291,6 +300,17 @@
) { page ->
val carouselItemInfo = remember { CarouselItemInfoImpl() }
val scope = remember { CarouselItemScopeImpl(itemInfo = carouselItemInfo) }
+ val clipShape = remember {
+ object : Shape {
+ override fun createOutline(
+ size: Size,
+ layoutDirection: LayoutDirection,
+ density: Density
+ ): Outline {
+ return Outline.Rectangle(carouselItemInfo.maskRect)
+ }
+ }
+ }
Box(
modifier = Modifier.carouselItem(
@@ -298,6 +318,7 @@
state = state,
strategy = { pageSize.strategy },
carouselItemInfo = carouselItemInfo,
+ clipShape = clipShape
)
) {
scope.content(page)
@@ -390,6 +411,9 @@
* @param state the carousel state
* @param strategy the strategy used to mask and translate items in the carousel
* @param carouselItemInfo the item info that should be updated with the changes in this modifier
+ * @param clipShape the shape the item will clip itself to. This should be a rectangle with a bounds
+ * that match the carousel item info's mask rect. Corner radii and other shape customizations can
+ * be done by the client using [CarouselItemScope.maskClip] and [CarouselItemScope.maskBorder].
*/
@OptIn(ExperimentalMaterial3Api::class)
internal fun Modifier.carouselItem(
@@ -397,6 +421,7 @@
state: CarouselState,
strategy: () -> Strategy,
carouselItemInfo: CarouselItemInfoImpl,
+ clipShape: Shape,
): Modifier {
return layout { measurable, constraints ->
val strategyResult = strategy.invoke()
@@ -480,33 +505,8 @@
carouselItemInfo.maskRectState = maskRect
// Clip the item
- clip = true
- shape = object : Shape {
- // TODO: Find a way to use the shape of the item set by the client for each item
- // TODO: Allow corner size customization
- val roundedCornerShape = RoundedCornerShape(ShapeDefaults.ExtraLarge.topStart)
- override fun createOutline(
- size: Size,
- layoutDirection: LayoutDirection,
- density: Density
- ): Outline {
- val cornerSize =
- roundedCornerShape.topStart.toPx(
- Size(maskRect.width, maskRect.height),
- density
- )
- val cornerRadius = CornerRadius(cornerSize)
- return Outline.Rounded(
- RoundRect(
- rect = maskRect,
- topLeft = cornerRadius,
- topRight = cornerRadius,
- bottomRight = cornerRadius,
- bottomLeft = cornerRadius
- )
- )
- }
- }
+ clip = maskRect != Rect(0f, 0f, size.width, size.height)
+ shape = clipShape
// After clipping, the items will have white space between them. Translate the
// items to pin their edges together
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselItemScope.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselItemScope.kt
index fea9940..47a7aaf 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselItemScope.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselItemScope.kt
@@ -16,7 +16,19 @@
package androidx.compose.material3.carousel
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.border
+import androidx.compose.foundation.shape.GenericShape
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.toRect
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.addOutline
+import androidx.compose.ui.platform.LocalDensity
/**
* Receiver scope for [Carousel] item content.
@@ -31,6 +43,38 @@
* in the composition.
*/
val carouselItemInfo: CarouselItemInfo
+
+ /**
+ * Clips the composable to the given [shape], taking into account the item's size in the cross
+ * axis and mask in the main axis.
+ *
+ * @param shape the shape to be applied to the composable
+ */
+ @Composable
+ fun Modifier.maskClip(shape: Shape): Modifier
+
+ /**
+ * Draw a border on the composable using the given [shape], taking into account the item's size
+ * in the cross axis and mask in the main axis.
+ *
+ * @param border the border to be drawn around the composable
+ * @param shape the shape of the border
+ */
+ @Composable
+ fun Modifier.maskBorder(border: BorderStroke, shape: Shape): Modifier
+
+ /**
+ * Converts and remembers [shape] into a [GenericShape] that uses the intersection of the
+ * carousel item's mask Rect and Size as the final shape's bounds.
+ *
+ * This method is useful if using a [Shape] in a Modifier other than [maskClip] and [maskBorder]
+ * where the shape should follow the changes in the item's mask size.
+ *
+ * @param shape The shape that will be converted and remembered and react to changes in the
+ * item's mask.
+ */
+ @Composable
+ fun rememberMaskShape(shape: Shape): GenericShape
}
@ExperimentalMaterial3Api
@@ -39,4 +83,26 @@
) : CarouselItemScope {
override val carouselItemInfo: CarouselItemInfo
get() = itemInfo
+
+ @Composable
+ override fun Modifier.maskClip(shape: Shape): Modifier =
+ clip(rememberMaskShape(shape = shape))
+
+ @Composable
+ override fun Modifier.maskBorder(
+ border: BorderStroke,
+ shape: Shape
+ ): Modifier = border(border, rememberMaskShape(shape = shape))
+
+ @Composable
+ override fun rememberMaskShape(shape: Shape): GenericShape {
+ val density = LocalDensity.current
+ return remember(carouselItemInfo, density) {
+ GenericShape { size, direction ->
+ val rect = carouselItemInfo.maskRect.intersect(size.toRect())
+ addOutline(shape.createOutline(rect.size, direction, density))
+ translate(Offset(rect.left, rect.top))
+ }
+ }
+ }
}