aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHunter Stich <hunterstich@google.com>2024-02-06 20:01:06 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2024-02-06 20:01:06 +0000
commitf3e658f3d732d5da8067cc4d03a21d34316529ff (patch)
treec8f9428eeebd249e38cbd6280d4bd896053c4e11
parentcc4cff9e2687ad5a2c41080b3e1bc32bdbb32d93 (diff)
parentbbe5a9ece4f6f6ebff4fe18ee12347002b37ad97 (diff)
downloadsupport-f3e658f3d732d5da8067cc4d03a21d34316529ff.tar.gz
Merge "[Carousel] Update top-level component composables to be orientation and variant specific." into androidx-main
-rw-r--r--compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt59
-rw-r--r--compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseStrategyTest.kt113
-rw-r--r--compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt131
-rw-r--r--compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt291
-rw-r--r--compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt2
-rw-r--r--compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keyline.kt95
-rw-r--r--compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/MultiBrowseStrategyProvider.kt108
-rw-r--r--compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt66
8 files changed, 427 insertions, 438 deletions
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
index 58c41be69b9..8868c742c23 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
@@ -22,6 +22,8 @@ import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicText
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.lightColorScheme
@@ -30,11 +32,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
@@ -86,25 +90,14 @@ class CarouselTest {
}
}
- @Test
- fun carousel_testInitialItem() {
- // Arrange
- createCarousel(initialItem = 5, orientation = Orientation.Horizontal)
-
- // Assert
- rule.runOnIdle {
- assertThat(carouselState.pagerState.currentPage).isEqualTo(5)
- }
- }
-
@Composable
internal fun Item(index: Int) {
Box(
modifier = Modifier
- .fillMaxSize()
- .background(Color.Blue)
- .testTag("$index")
- .focusable(),
+ .fillMaxSize()
+ .background(Color.Blue)
+ .testTag("$index")
+ .focusable(),
contentAlignment = Alignment.Center
) {
BasicText(text = index.toString())
@@ -114,30 +107,30 @@ class CarouselTest {
private fun createCarousel(
initialItem: Int = 0,
itemCount: () -> Int = { DefaultItemCount },
- modifier: Modifier = Modifier,
+ modifier: Modifier = Modifier.width(412.dp).height(221.dp),
orientation: Orientation = Orientation.Horizontal,
content: @Composable CarouselScope.(item: Int) -> Unit = { Item(index = it) }
) {
rule.setMaterialContent(lightColorScheme()) {
- val state = rememberCarouselState(
- initialItem = initialItem,
- itemCount = itemCount,
- ).also {
+ val state = rememberCarouselState(initialItem, itemCount).also {
carouselState = it
}
- if (orientation == Orientation.Horizontal) {
- HorizontalCarousel(
- state = state,
- modifier = modifier.testTag(CarouselTestTag),
- content = content,
- )
- } else {
- VerticalCarousel(
- state = state,
- modifier = modifier.testTag(CarouselTestTag),
- content = content,
- )
- }
+ val density = LocalDensity.current
+ Carousel(
+ state = state,
+ orientation = orientation,
+ keylineList = { multiBrowseKeylineList(
+ density,
+ state.pagerState.layoutInfo.viewportSize.let {
+ if (orientation == Orientation.Horizontal) it.width else it.height
+ }.toFloat(),
+ preferredItemSize = with(density) { 186.dp.toPx() },
+ itemSpacing = 0f
+ ) },
+ modifier = modifier.testTag(CarouselTestTag),
+ itemSpacing = 0.dp,
+ content = content,
+ )
}
}
}
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseStrategyTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseStrategyTest.kt
deleted file mode 100644
index 8e0f286a03c..00000000000
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseStrategyTest.kt
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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 androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.dp
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class MultiBrowseStrategyTest {
-
- private val Density = Density(1f)
-
- @Test
- fun testStrategy_doesntResizeLargeWhenEnoughRoom() {
-
- val itemSize = 120.dp // minSmallItemSize = 40.dp * 3
- val strategyProvider = MultiBrowseStrategyProvider(itemSize)
- val strategy = strategyProvider.createStrategy(
- density = Density,
- carouselMainAxisSize = 500f,
- itemSpacing = 0)
-
- assertThat(strategy?.itemMainAxisSize).isEqualTo(with(Density) { itemSize.toPx() })
- }
-
- @Test
- fun testStrategy_resizesItemLargerThanContainerToFit1Small() {
- val itemSize = 200f
- val strategyProvider = MultiBrowseStrategyProvider(with(Density) { itemSize.toDp() })
- val strategy = strategyProvider.createStrategy(
- density = Density,
- carouselMainAxisSize = 100f,
- itemSpacing = 0)
-
- val minSmallItemSize: Float = with(Density) { StrategyDefaults.minSmallSize.toPx() }
- val keylines = strategy?.getDefaultKeylines()
-
- // If the item size given is larger than the container, the adjusted keyline list from
- // the MultibrowseStrategy should be [xSmall-Large-Small-xSmall]
- assertThat(strategy?.itemMainAxisSize).isAtMost(100f)
- assertThat(keylines).hasSize(4)
- assertThat(keylines?.get(0)?.unadjustedOffset).isLessThan(0f)
- assertThat(keylines?.get(keylines.lastIndex)?.unadjustedOffset).isGreaterThan(100f)
- assertThat(keylines?.get(1)?.isFocal).isTrue()
- assertThat(keylines?.get(2)?.size).isEqualTo(minSmallItemSize)
- }
-
- @Test
- fun testStrategy_hasNoSmallItemsIfNotEnoughRoom() {
- val minSmallItemSize: Float = with(Density) { StrategyDefaults.minSmallSize.toPx() }
- val strategyProvider = MultiBrowseStrategyProvider(with(Density) { 200f.toDp() })
- val strategy = strategyProvider.createStrategy(
- density = Density,
- carouselMainAxisSize = minSmallItemSize,
- itemSpacing = 0)
- val keylines = strategy?.getDefaultKeylines()
-
- assertThat(strategy?.itemMainAxisSize).isEqualTo(minSmallItemSize)
- assertThat(keylines?.firstFocal == keylines?.firstNonAnchor)
- assertThat(keylines?.lastFocal == keylines?.lastNonAnchor)
- }
-
- @Test
- fun testStrategy_isNullIfAvailableSpaceIsZero() {
- val strategyProvider = MultiBrowseStrategyProvider(with(Density) { 200f.toDp() })
- val strategy = strategyProvider.createStrategy(
- density = Density,
- carouselMainAxisSize = 0F,
- itemSpacing = 0)
-
- assertThat(strategy).isNull()
- }
-
- @Test
- fun testStrategy_adjustsMediumSizeToBeProportional() {
- val maxSmallItemSize: Float = with(Density) { StrategyDefaults.maxSmallSize.toPx() }
- val targetItemSize = 200f
- val carouselSize = targetItemSize * 2 + maxSmallItemSize * 2
- val strategyProvider = MultiBrowseStrategyProvider(with(Density) { targetItemSize.toDp() })
- val strategy = strategyProvider.createStrategy(
- density = Density,
- carouselMainAxisSize = carouselSize,
- itemSpacing = 0)
- val keylines = strategy?.getDefaultKeylines()
-
- // Assert that there's only one small item, and a medium item that has a size between
- // the large and small items
- // We expect a keyline list of [xSmall-Large-Large-Medium-Small-xSmall]
- assertThat(keylines).hasSize(6)
- assertThat(keylines?.get(1)?.isFocal).isTrue()
- assertThat(keylines?.get(2)?.isFocal).isTrue()
- assertThat(keylines?.get(3)?.size).isLessThan(keylines?.get(2)?.size)
- assertThat(keylines?.get(4)?.size).isLessThan(keylines?.get(3)?.size)
- }
-}
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
new file mode 100644
index 00000000000..2b6d054dec3
--- /dev/null
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
@@ -0,0 +1,131 @@
+/*
+ * 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 androidx.compose.ui.unit.Density
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class MultiBrowseTest {
+
+ private val density = Density(1f)
+
+ @Test
+ fun testMultiBrowse_doesNotResizeLargeWhenEnoughRoom() {
+ val itemSize = 120f // minSmallItemSize = 40.dp * 3
+ val keylineList = multiBrowseKeylineList(
+ density = density,
+ carouselMainAxisSize = 500f,
+ preferredItemSize = itemSize,
+ itemSpacing = 0f
+ )!!
+ val strategy = Strategy.create(
+ carouselMainAxisSize = 500f,
+ keylineList = keylineList
+ )
+
+ assertThat(strategy.itemMainAxisSize).isEqualTo(itemSize)
+ }
+
+ @Test
+ fun testMultiBrowse_resizesItemLargerThanContainerToFit1Small() {
+ val itemSize = 200f
+ val keylineList = multiBrowseKeylineList(
+ density = density,
+ carouselMainAxisSize = 100f,
+ preferredItemSize = itemSize,
+ itemSpacing = 0f
+ )!!
+
+ val strategy = Strategy.create(
+ carouselMainAxisSize = 100f,
+ keylineList = keylineList
+ )
+ val minSmallItemSize: Float = with(density) { StrategyDefaults.minSmallSize.toPx() }
+ val keylines = strategy.getDefaultKeylines()
+
+ // If the item size given is larger than the container, the adjusted keyline list from
+ // the multi-browse keyline list should be [xSmall-Large-Small-xSmall]
+ assertThat(strategy.itemMainAxisSize).isAtMost(100f)
+ assertThat(keylines).hasSize(4)
+ assertThat(keylines[0].unadjustedOffset).isLessThan(0f)
+ assertThat(keylines[keylines.lastIndex].unadjustedOffset).isGreaterThan(100f)
+ assertThat(keylines[1].isFocal).isTrue()
+ assertThat(keylines[2].size).isEqualTo(minSmallItemSize)
+ }
+
+ @Test
+ fun testMultiBrowse_hasNoSmallItemsIfNotEnoughRoom() {
+ val minSmallItemSize: Float = with(density) { StrategyDefaults.minSmallSize.toPx() }
+ val keylineList = multiBrowseKeylineList(
+ density = density,
+ carouselMainAxisSize = minSmallItemSize,
+ preferredItemSize = 200f,
+ itemSpacing = 0f
+ )!!
+ val strategy = Strategy.create(
+ carouselMainAxisSize = minSmallItemSize,
+ keylineList = keylineList
+ )
+ val keylines = strategy.getDefaultKeylines()
+
+ assertThat(strategy.itemMainAxisSize).isEqualTo(minSmallItemSize)
+ assertThat(keylines.firstFocal == keylines.firstNonAnchor)
+ assertThat(keylines.lastFocal == keylines.lastNonAnchor)
+ }
+
+ @Test
+ fun testMultiBrowse_isNullIfAvailableSpaceIsZero() {
+ val keylineList = multiBrowseKeylineList(
+ density = density,
+ carouselMainAxisSize = 0f,
+ preferredItemSize = 200f,
+ itemSpacing = 0f
+ )
+ assertThat(keylineList).isNull()
+ }
+
+ @Test
+ fun testMultiBrowse_adjustsMediumSizeToBeProportional() {
+ val maxSmallItemSize: Float = with(density) { StrategyDefaults.maxSmallSize.toPx() }
+ val preferredItemSize = 200f
+ val carouselSize = preferredItemSize * 2 + maxSmallItemSize * 2
+ val keylineList = multiBrowseKeylineList(
+ density = density,
+ carouselMainAxisSize = carouselSize,
+ preferredItemSize = preferredItemSize,
+ itemSpacing = 0f
+ )!!
+ val strategy = Strategy.create(
+ carouselMainAxisSize = carouselSize,
+ keylineList = keylineList
+ )
+ val keylines = strategy.getDefaultKeylines()
+
+ // Assert that there's only one small item, and a medium item that has a size between
+ // the large and small items
+ // We expect a keyline list of [xSmall-Large-Large-Medium-Small-xSmall]
+ assertThat(keylines).hasSize(6)
+ assertThat(keylines[1].isFocal).isTrue()
+ assertThat(keylines[2].isFocal).isTrue()
+ assertThat(keylines[3].size).isLessThan(keylines[2].size)
+ assertThat(keylines[4].size).isLessThan(keylines[3].size)
+ }
+}
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 f27a6b6b282..8982afe8afd 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
@@ -17,6 +17,8 @@
package androidx.compose.material3.carousel
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PageSize
import androidx.compose.foundation.pager.VerticalPager
@@ -33,31 +35,166 @@ import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
/**
- * A enumeration of ways items can be aligned along a carousel's main axis
+ * <a href=https://m3.material.io/components/carousel/overview" class="external" target="_blank">Material Design Carousel</a>
+ *
+ * A horizontal carousel meant to display many items at once for quick browsing of smaller content
+ * like album art or photo thumbnails.
+ *
+ * Note that this carousel may adjust the size of large items. In order to ensure a mix of large,
+ * medium, and small items fit perfectly into the available space and are arranged in a
+ * visually pleasing way, this carousel finds the nearest number of large items that
+ * will fit the container and adjusts their size to fit, if necessary.
+ *
+ * For more information, see <a href="https://material.io/components/carousel/overview">design
+ * guidelines</a>.
+ *
+ * @param state The state object to be used to control the carousel's state.
+ * @param preferredItemSize The size fully visible items would like to be in the main axis. This
+ * size is a target and will likely be adjusted by carousel in order to fit a whole number of
+ * items within the container.
+ * @param modifier A modifier instance to be applied to this carousel outer layout
+ * @param itemSpacing The amount of space used to separate items in the carousel
+ * @param minSmallSize The minimum allowable size of small masked items
+ * @param maxSmallSize The maximum allowable size of small masked items
+ * @param content The carousel's content Composable.
+ *
+ * TODO: Add sample link
*/
-internal enum class CarouselAlignment {
- /** Start aligned carousels place focal items at the start/top of the container */
- Start,
- /** Center aligned carousels place focal items in the middle of the container */
- Center,
- /** End aligned carousels place focal items at the end/bottom of the container */
- End
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalFoundationApi::class)
+@ExperimentalMaterial3Api
+@Composable
+internal fun HorizontalMultiBrowseCarousel(
+ state: CarouselState,
+ preferredItemSize: Dp,
+ modifier: Modifier = Modifier,
+ itemSpacing: Dp = 0.dp,
+ minSmallSize: Dp = StrategyDefaults.minSmallSize,
+ maxSmallSize: Dp = StrategyDefaults.maxSmallSize,
+ content: @Composable CarouselScope.(item: Int) -> Unit
+) {
+ val density = LocalDensity.current
+ Carousel(
+ state = state,
+ orientation = Orientation.Horizontal,
+ keylineList = {
+ with(density) {
+ multiBrowseKeylineList(
+ density = this,
+ carouselMainAxisSize = state.pagerState.layoutInfo.viewportSize.width.toFloat(),
+ preferredItemSize = preferredItemSize.toPx(),
+ itemSpacing = itemSpacing.toPx(),
+ minSmallSize = minSmallSize.toPx(),
+ maxSmallSize = maxSmallSize.toPx()
+ )
+ }
+ },
+ modifier = modifier,
+ itemSpacing = itemSpacing,
+ content = content
+ )
}
/**
- * An enumeration of orientations that determine a carousel's main axis
+ * <a href=https://m3.material.io/components/carousel/overview" class="external" target="_blank">Material Design Carousel</a>
+ *
+ * Carousels contain a collection of items that changes sizes according to their placement and the
+ * chosen strategy.
+ *
+ * @param state The state object to be used to control the carousel's state.
+ * @param modifier A modifier instance to be applied to this carousel outer layout
+ * @param keylineList The list of keylines that are fixed positions along the scrolling axis which
+ * define the state an item should be in when its center is co-located with the keyline's position.
+ * @param itemSpacing The amount of space used to separate items in the carousel
+ * @param orientation The layout orientation of the carousel
+ * @param content The carousel's content Composable where each call is passed the index, from the
+ * total item count, of the item being composed
+ * TODO: Add sample link
*/
-internal enum class Orientation {
- /** Vertical orientation representing Y axis */
- Vertical,
- /** Horizontal orientation representing X axis */
- Horizontal
+// TODO: b/321997456 - Remove lint suppression once version checks are added in lint or library
+// moves to beta
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalFoundationApi::class)
+@ExperimentalMaterial3Api
+@Composable
+internal fun Carousel(
+ state: CarouselState,
+ orientation: Orientation,
+ keylineList: () -> KeylineList?,
+ modifier: Modifier = Modifier,
+ itemSpacing: Dp = 0.dp,
+ content: @Composable CarouselScope.(itemIndex: Int) -> Unit
+) {
+ val availableSpace = if (orientation == Orientation.Horizontal) {
+ state.pagerState.layoutInfo.viewportSize.width.toFloat()
+ } else {
+ state.pagerState.layoutInfo.viewportSize.height.toFloat()
+ }
+ val keylines: KeylineList? = keylineList.invoke()
+ val strategy = if (keylines != null) {
+ Strategy.create(availableSpace, keylines)
+ } else {
+ null
+ }
+
+ val pageSize = PageSize.Fixed(
+ with(LocalDensity.current) { strategy?.itemMainAxisSize?.toDp() } ?: 0.dp
+ )
+
+ // TODO: Update beyond bounds numbers according to Strategy
+ val outOfBoundsPageCount = 2
+ val carouselScope = CarouselScopeImpl
+
+ if (orientation == Orientation.Horizontal) {
+ HorizontalPager(
+ state = state.pagerState,
+ pageSize = pageSize,
+ pageSpacing = itemSpacing,
+ outOfBoundsPageCount = outOfBoundsPageCount,
+ modifier = modifier
+ ) { page ->
+ Box(modifier = Modifier.carouselItem(page, state, strategy)) {
+ carouselScope.content(page)
+ }
+ }
+ } else if (orientation == Orientation.Vertical) {
+ VerticalPager(
+ state = state.pagerState,
+ pageSize = pageSize,
+ pageSpacing = itemSpacing,
+ outOfBoundsPageCount = outOfBoundsPageCount,
+ modifier = modifier
+ ) { page ->
+ Box(modifier = Modifier.carouselItem(page, state, strategy)) {
+ carouselScope.content(page)
+ }
+ }
+ }
+}
+
+/**
+ * This class defines ways items can be aligned along a carousel's main axis.
+ */
+@JvmInline
+internal value class CarouselAlignment private constructor(internal val value: Int) {
+ companion object {
+ /** Start aligned carousels place focal items at the start/top of the container */
+ val Start = CarouselAlignment(-1)
+
+ /** Center aligned carousels place focal items in the middle of the container */
+ val Center = CarouselAlignment(0)
+
+ /** End aligned carousels place focal items at the end/bottom of the container */
+ val End = CarouselAlignment(1)
+ }
}
/**
@@ -65,25 +202,31 @@ internal enum class Orientation {
* of a Carousel.
*
* @param index the index of the item in the carousel
- * @param viewportSize the size of the carousel container
- * @param orientation the orientation of the carousel
- * @param itemsCount the total number of items in the carousel
- * @param scrollOffset the amount the carousel has been scrolled in pixels
+ * @param state the carousel state
* @param strategy the strategy used to mask and translate items in the carousel
*/
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
internal fun Modifier.carouselItem(
index: Int,
- itemsCount: Int,
- viewportSize: IntSize,
- orientation: Orientation,
- scrollOffset: Float,
- strategy: Strategy
+ state: CarouselState,
+ strategy: Strategy?
): Modifier {
+ val viewportSize = state.pagerState.layoutInfo.viewportSize
+ val orientation = state.pagerState.layoutInfo.orientation
val isVertical = orientation == Orientation.Vertical
val mainAxisCarouselSize = if (isVertical) viewportSize.height else viewportSize.width
- if (mainAxisCarouselSize == 0) {
+
+ if (mainAxisCarouselSize == 0 || strategy == null) {
return this
}
+ // Scroll offset calculation using currentPage and currentPageOffsetFraction
+ val firstVisibleItemScrollOffset =
+ state.pagerState.currentPageOffsetFraction * strategy.itemMainAxisSize
+ val scrollOffset = (state.pagerState.currentPage * strategy.itemMainAxisSize) +
+ firstVisibleItemScrollOffset
+ val itemsCount = state.pagerState.pageCount
+
val maxScrollOffset =
itemsCount * strategy.itemMainAxisSize - mainAxisCarouselSize
val keylines = strategy.getKeylineListForScrollOffset(scrollOffset, maxScrollOffset)
@@ -203,101 +346,3 @@ private fun getProgress(before: Keyline, after: Keyline, unadjustedOffset: Float
val total = after.unadjustedOffset - before.unadjustedOffset
return (unadjustedOffset - before.unadjustedOffset) / total
}
-
-/**
- * <a href=https://m3.material.io/components/carousel/overview" class="external" target="_blank">Material Design Carousel</a>
- *
- * A Carousel that scrolls horizontally. Carousels contain a collection of items that changes sizes
- * according to their placement and the chosen strategy.
- *
- * @param state The state object to be used to control the carousel's state.
- * @param modifier A modifier instance to be applied to this carousel outer layout
- * @param content The carousel's content Composable.
- * TODO: Add sample link
- */
-@ExperimentalMaterial3Api
-@Composable
-internal fun HorizontalCarousel(
- state: CarouselState,
- modifier: Modifier = Modifier,
- content: @Composable CarouselScope.(item: Int) -> Unit
-) = Carousel(
- state = state,
- modifier = modifier,
- orientation = Orientation.Horizontal,
- content = content
-)
-
-/**
- * <a href=https://m3.material.io/components/carousel/overview" class="external" target="_blank">Material Design Carousel</a>
- *
- * A Carousel that scrolls vertically. Carousels contain a collection of items that changes sizes
- * according to their placement and the chosen strategy.
- *
- * @param state The state object to be used to control the carousel's state.
- * @param modifier A modifier instance to be applied to this carousel outer layout
- * @param content The carousel's content Composable.
- * TODO: Add sample link
- */
-@ExperimentalMaterial3Api
-@Composable
-internal fun VerticalCarousel(
- state: CarouselState,
- modifier: Modifier = Modifier,
- content: @Composable CarouselScope.(item: Int) -> Unit
-) = Carousel(
- state = state,
- modifier = modifier,
- orientation = Orientation.Vertical,
- content = content
-)
-
-/**
- * <a href=https://m3.material.io/components/carousel/overview" class="external" target="_blank">Material Design Carousel</a>
- *
- * Carousels contain a collection of items that changes sizes according to their placement and the
- * chosen strategy.
- *
- * @param state The state object to be used to control the carousel's state.
- * @param modifier A modifier instance to be applied to this carousel outer layout
- * @param orientation The layout orientation of the carousel
- * @param content The carousel's content Composable.
- * TODO: Add sample link
- */
-// TODO: b/321997456 - Remove lint suppression once version checks are added in lint or library
-// moves to beta
-@Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalFoundationApi::class)
-@ExperimentalMaterial3Api
-@Composable
-internal fun Carousel(
- state: CarouselState,
- modifier: Modifier = Modifier,
- orientation: Orientation = Orientation.Horizontal,
- content: @Composable CarouselScope.(item: Int) -> Unit
-) {
- // TODO: Update page size according to strategy
- val pageSize = PageSize.Fill
- // TODO: Update out of bounds page count numbers
- val outOfBoundsPageCount = 1
- val carouselScope = CarouselScopeImpl
- if (orientation == Orientation.Horizontal) {
- HorizontalPager(
- state = state.pagerState,
- pageSize = pageSize,
- outOfBoundsPageCount = outOfBoundsPageCount,
- modifier = modifier
- ) { page ->
- carouselScope.content(page)
- }
- } else if (orientation == Orientation.Vertical) {
- VerticalPager(
- state = state.pagerState,
- pageSize = pageSize,
- outOfBoundsPageCount = outOfBoundsPageCount,
- modifier = modifier
- ) { page ->
- carouselScope.content(page)
- }
- }
-}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
index e48ec30f04d..65b7ac4f665 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/CarouselState.kt
@@ -29,7 +29,7 @@ import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
/**
- * The state that can be used to control [VerticalCarousel] and [HorizontalCarousel].
+ * The state that can be used to control all types of carousels.
*
* @param currentItem the current item to be scrolled to.
* @param currentItemOffsetFraction the current item offset as a fraction of the item size.
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keyline.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keyline.kt
index eeda9e6375c..c4451375005 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keyline.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keyline.kt
@@ -16,9 +16,101 @@
package androidx.compose.material3.carousel
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastMapIndexed
import kotlin.math.abs
+import kotlin.math.ceil
+import kotlin.math.floor
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Creates a list of keylines that arranges items into a multi-browse configuration.
+ *
+ * Note that this function may adjust the size of large items. In order to ensure large, medium,
+ * and small items fit perfectly into the available space and are numbered/arranged in a
+ * visually pleasing and opinionated way, this strategy finds the nearest number of large items that
+ * will fit into an approved arrangement that requires the least amount of size adjustment
+ * necessary.
+ *
+ * For more information, see <a href="https://material.io/components/carousel/overview">design
+ * guidelines</a>.
+ *
+ * @param density The [Density] object that provides pixel density information of the device
+ * @param carouselMainAxisSize the size of the carousel container, in pixels, in the main
+ * scrolling axis
+ * @param preferredItemSize the desired size of large items, in pixels, in the main scrolling axis
+ * @param itemSpacing the spacing between items in pixels
+ * @param minSmallSize the minimum allowable size of small items in pixels
+ * @param maxSmallSize the maximum allowable size of small items in pixels
+ */
+internal fun multiBrowseKeylineList(
+ density: Density,
+ carouselMainAxisSize: Float,
+ preferredItemSize: Float,
+ itemSpacing: Float,
+ minSmallSize: Float = with(density) { StrategyDefaults.minSmallSize.toPx() },
+ maxSmallSize: Float = with(density) { StrategyDefaults.maxSmallSize.toPx() },
+): KeylineList? {
+ if (carouselMainAxisSize == 0f || preferredItemSize == 0f) {
+ return null
+ }
+
+ var smallCounts: IntArray = intArrayOf(1)
+ val mediumCounts: IntArray = intArrayOf(1, 0)
+
+ val targetLargeSize: Float = min(preferredItemSize + itemSpacing, carouselMainAxisSize)
+ // Ideally we would like to create a balanced arrangement where a small item is 1/3 the size
+ // of the large item and medium items are sized between large and small items. Clamp the
+ // small target size within our min-max range and as close to 1/3 of the target large item
+ // size as possible.
+ val targetSmallSize: Float = (targetLargeSize / 3f + itemSpacing).coerceIn(
+ minSmallSize + itemSpacing,
+ maxSmallSize + itemSpacing
+ )
+ val targetMediumSize = (targetLargeSize + targetSmallSize) / 2f
+
+ if (carouselMainAxisSize < minSmallSize * 2) {
+ // If the available space is too small to fit a large item and small item (where a large
+ // item is bigger than a small item), allow arrangements with
+ // no small items.
+ smallCounts = intArrayOf(0)
+ }
+
+ // Find the minimum space left for large items after filling the carousel with the most
+ // permissible medium and small items to determine a plausible minimum large count.
+ val minAvailableLargeSpace = carouselMainAxisSize - targetMediumSize * mediumCounts.max() -
+ maxSmallSize * smallCounts.max()
+ val minLargeCount = max(
+ 1,
+ floor(minAvailableLargeSpace / targetLargeSize).toInt())
+ val maxLargeCount = ceil(carouselMainAxisSize / targetLargeSize).toInt()
+
+ val largeCounts = IntArray(maxLargeCount - minLargeCount + 1) { maxLargeCount - it }
+ val anchorSize = with(density) { StrategyDefaults.anchorSize.toPx() }
+ val arrangement = Arrangement.findLowestCostArrangement(
+ availableSpace = carouselMainAxisSize,
+ targetSmallSize = targetSmallSize,
+ minSmallSize = minSmallSize,
+ maxSmallSize = maxSmallSize,
+ smallCounts = smallCounts,
+ targetMediumSize = targetMediumSize,
+ mediumCounts = mediumCounts,
+ targetLargeSize = targetLargeSize,
+ largeCounts = largeCounts,
+ ) ?: return null
+
+ return keylineListOf(carouselMainAxisSize, CarouselAlignment.Start) {
+ add(anchorSize, isAnchor = true)
+
+ repeat(arrangement.largeCount) { add(arrangement.largeSize) }
+ repeat(arrangement.mediumCount) { add(arrangement.mediumSize) }
+ repeat(arrangement.smallCount) { add(arrangement.smallSize) }
+
+ add(anchorSize, isAnchor = true)
+ }
+}
/**
* A structure that is fixed at a specific [offset] along a scrolling axis and
@@ -294,11 +386,12 @@ private class KeylineListScopeImpl : KeylineListScope {
pivotIndex = firstFocalIndex
pivotOffset = when (carouselAlignment) {
- CarouselAlignment.Start -> focalItemSize / 2
CarouselAlignment.Center -> {
(carouselMainAxisSize / 2) - ((focalItemSize / 2) * focalItemCount)
}
CarouselAlignment.End -> carouselMainAxisSize - (focalItemSize / 2)
+ // Else covers and defaults to CarouselAlignment.Start
+ else -> focalItemSize / 2
}
val keylines = createKeylinesWithPivot(
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/MultiBrowseStrategyProvider.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/MultiBrowseStrategyProvider.kt
deleted file mode 100644
index f8d4db6626e..00000000000
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/MultiBrowseStrategyProvider.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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 androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import kotlin.math.ceil
-import kotlin.math.floor
-import kotlin.math.max
-import kotlin.math.min
-
-/**
- * A [StrategyProvider] that provides the multi-browse strategy, which fits large, medium, and small
- * items into a layout for quick browsing of multiple items at once.
- *
- * Note that this strategy may adjust the size of large items. In order to ensure large, medium,
- * and small items fit perfectly into the available space and are numbered/arranged in a
- * visually pleasing and opinionated way, this strategy finds the nearest number of large items that
- * will fit into an approved arrangement that requires the least amount of size adjustment
- * necessary.
- *
- * For more information, see <a href="https://material.io/components/carousel/overview">design
- * guidelines</a>.
- */
-internal class MultiBrowseStrategyProvider(
- private val targetLargeItemMainAxisSize: Dp,
- private val minSmallSize: Dp = StrategyDefaults.minSmallSize,
- private val maxSmallSize: Dp = StrategyDefaults.maxSmallSize
-) :
- StrategyProvider() {
-
- override fun createStrategy(
- density: Density,
- carouselMainAxisSize: Float,
- itemSpacing: Int,
- ): Strategy? {
- if (carouselMainAxisSize == 0f || targetLargeItemMainAxisSize == 0.dp) {
- return null
- }
-
- val targetLargeItemSize = with(density) { targetLargeItemMainAxisSize.toPx() }
- val minSmallItemSize = with(density) { minSmallSize.toPx() }
- val maxSmallItemSize = with(density) { maxSmallSize.toPx() }
- var smallCounts: IntArray = intArrayOf(1)
- val mediumCounts: IntArray = intArrayOf(1, 0)
-
- val targetLargeSize: Float = min(targetLargeItemSize + itemSpacing, carouselMainAxisSize)
- // Ideally we would like to create a balanced arrangement where a small item is 1/3 the size
- // of the large item and medium items are sized between large and small items. Clamp the
- // small target size within our min-max range and as close to 1/3 of the target large item
- // size as possible.
- val targetSmallSize: Float = (targetLargeItemSize / 3f + itemSpacing).coerceIn(
- minSmallItemSize + itemSpacing,
- maxSmallItemSize + itemSpacing
- )
- val targetMediumSize = (targetLargeSize + targetSmallSize) / 2f
-
- if (carouselMainAxisSize < minSmallItemSize * 2) {
- // If the available space is too small to fit a large item and small item (where a large
- // item is bigger than a small item), allow arrangements with
- // no small items.
- smallCounts = intArrayOf(0)
- }
-
- // Find the minimum space left for large items after filling the carousel with the most
- // permissible medium and small items to determine a plausible minimum large count.
- val minAvailableLargeSpace = carouselMainAxisSize - targetMediumSize * mediumCounts.max() -
- maxSmallItemSize * smallCounts.max()
- val minLargeCount = max(
- 1,
- floor(minAvailableLargeSpace / targetLargeSize).toInt())
- val maxLargeCount = ceil(carouselMainAxisSize / targetLargeSize).toInt()
-
- val largeCounts = IntArray(maxLargeCount - minLargeCount + 1) { maxLargeCount - it }
- val arrangement = Arrangement.findLowestCostArrangement(
- availableSpace = carouselMainAxisSize,
- targetSmallSize = targetSmallSize,
- minSmallSize = minSmallItemSize,
- maxSmallSize = maxSmallItemSize,
- smallCounts = smallCounts,
- targetMediumSize = targetMediumSize,
- mediumCounts = mediumCounts,
- targetLargeSize = targetLargeSize,
- largeCounts = largeCounts,
- ) ?: return null
-
- return createStartAlignedStrategy(
- availableSpace = carouselMainAxisSize,
- arrangement = arrangement,
- anchorSize = with(density) { StrategyDefaults.anchorSize.toPx() }
- )
- }
-}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
index 484c6ad0816..8ca58e4bb6f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
@@ -19,7 +19,6 @@ package androidx.compose.material3.carousel
import androidx.annotation.VisibleForTesting
import androidx.collection.FloatList
import androidx.collection.mutableFloatListOf
-import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.lerp
@@ -35,68 +34,17 @@ internal object StrategyDefaults {
}
/**
- * Helper method to create a default start-aligned [Strategy] contained within the bounds of
- * [availableSpace] and based on the given [Arrangement].
- *
- * @param availableSpace the available space to contain the [Strategy] within
- * @param arrangement the arrangement containing information on the sizes and counts of the
- * items in the [Strategy].
- * @param anchorSize the size that the anchor keylines should be in the strategy. The smaller
- * this is, the more the item will shrink as it moves off-screen.
- */
-internal fun createStartAlignedStrategy(
- availableSpace: Float,
- arrangement: Arrangement,
- anchorSize: Float,
-): Strategy {
- val keylineList = keylineListOf(availableSpace, CarouselAlignment.Start) {
- add(anchorSize, isAnchor = true)
-
- repeat(arrangement.largeCount) { add(arrangement.largeSize) }
- repeat(arrangement.mediumCount) { add(arrangement.mediumSize) }
- repeat(arrangement.smallCount) { add(arrangement.smallSize) }
-
- add(anchorSize, isAnchor = true)
- }
-
- return Strategy.create(availableSpace, keylineList)
-}
-
-/**
- * A class that provides [Strategy] instances to a scrollable component.
- *
- * [StrategyProvider.createStrategy] will be called any time properties which affect a carousel's
- * arrangement change. It is the implementation's responsibility to create an arrangement for the
- * given parameters and return a [Strategy] by calling [Strategy.create].
- */
-internal sealed class StrategyProvider() {
-
- /**
- * Create and return a new [Strategy] for the given carousel size.
- *
- * @param density The current density value
- * @param carouselMainAxisSize the size of the carousel in the main axis in pixels
- * @param itemSpacing The spacing in between the items that are not a part of the item size
- */
- internal abstract fun createStrategy(
- density: Density,
- carouselMainAxisSize: Float,
- itemSpacing: Int,
- ): Strategy?
-}
-
-/**
* A class which supplies carousel with the appropriate [KeylineList] for any given scroll offset.
*
* All items in a carousel need the opportunity to pass through the focal keyline range. Depending
* on where the focal range is located within the scrolling container, some items, like those at
* the beginning or end of the list, might not reach the focal range. To account for this,
* [Strategy] manages shifting the focal keylines to the beginning of the list when scrolled an
- * offset of 0 and the end of the list when scrolled to the list's max offset. [StrategyProvider]
- * needs only to create a "default" [KeylineList] (where keylines should be placed when scrolling
- * in the middle of the list) and call [Strategy.create] to have [Strategy] generate the steps
- * needed to move the focal range to the start and end of the scroll container. When scrolling, the
- * scrollable component can access the correct [KeylineList] for any given offset using
+ * offset of 0 and the end of the list when scrolled to the list's max offset. The only thing
+ * [Strategy] needs is a "default" [KeylineList] (where keylines should be placed when scrolling
+ * in the middle of the list) and [Strategy.create] will generate the steps needed to move the
+ * focal range to the start and end of the scroll container. When scrolling, the scrollable
+ * component can access the correct [KeylineList] for any given offset using
* [getKeylineListForScrollOffset].
*
* @param defaultKeylines the [KeylineList] used when anywhere in the center of the list
@@ -211,7 +159,6 @@ internal class Strategy private constructor(
/**
* Creates a new [Strategy] based on a default [keylineList].
- *
* The [keylineList] passed to this method will be the keylines used when the carousel is
* scrolled anywhere in the middle of the list (not the beginning or end). From these
* default keylines, additional [KeylineList]s will be created which move the focal range
@@ -501,7 +448,8 @@ internal class Strategy private constructor(
return ShiftPointRange(
fromStepIndex = 0,
toStepIndex = 0,
- steppedInterpolation = 0f)
+ steppedInterpolation = 0f
+ )
}
private fun MutableList<Keyline>.move(srcIndex: Int, dstIndex: Int): MutableList<Keyline> {