diff --git a/Backpack/src/main/java/net/skyscanner/backpack/bottomnav/BpkBottomNav.kt b/Backpack/src/main/java/net/skyscanner/backpack/bottomnav/BpkBottomNav.kt index 2e53b65608..4e51a6ce68 100644 --- a/Backpack/src/main/java/net/skyscanner/backpack/bottomnav/BpkBottomNav.kt +++ b/Backpack/src/main/java/net/skyscanner/backpack/bottomnav/BpkBottomNav.kt @@ -61,7 +61,7 @@ open class BpkBottomNav @JvmOverloads constructor( Menu.NONE, id, menu.size(), - SpannableStringBuilder().append(title, fontSpan, Spannable.SPAN_INCLUSIVE_INCLUSIVE), + wrapFontSpan(title), ) .setIcon(icon) @@ -100,6 +100,13 @@ open class BpkBottomNav @JvmOverloads constructor( throw UnsupportedOperationException("Not supported") } + private fun wrapFontSpan(text: CharSequence): CharSequence = + if (resources.configuration.layoutDirection == LAYOUT_DIRECTION_LTR) { + SpannableStringBuilder().append(text, fontSpan, Spannable.SPAN_INCLUSIVE_INCLUSIVE) + } else { + text + } + private class ListenersDelegate( private val menu: Menu, ) : OnItemSelectedListener, OnItemReselectedListener { diff --git a/Backpack/src/main/java/net/skyscanner/backpack/skeleton/BpkCircleSkeleton.kt b/Backpack/src/main/java/net/skyscanner/backpack/skeleton/BpkCircleSkeleton.kt index e50ceb31d2..35b96c996b 100644 --- a/Backpack/src/main/java/net/skyscanner/backpack/skeleton/BpkCircleSkeleton.kt +++ b/Backpack/src/main/java/net/skyscanner/backpack/skeleton/BpkCircleSkeleton.kt @@ -105,10 +105,10 @@ class BpkCircleSkeleton @JvmOverloads constructor( super.onLayout(changed, 0, 0, internalSize, internalSize) } - override fun onDraw(canvas: Canvas?) { + override fun onDraw(canvas: Canvas) { val internalSize = getInternalSize() val radius = internalSize.toFloat().div(2) - canvas?.drawCircle(radius, radius, radius, paint) + canvas.drawCircle(radius, radius, radius, paint) } private companion object { diff --git a/Backpack/src/main/java/net/skyscanner/backpack/skeleton/BpkHeadlineSkeleton.kt b/Backpack/src/main/java/net/skyscanner/backpack/skeleton/BpkHeadlineSkeleton.kt index 68b7a5afa0..6b6061d80f 100644 --- a/Backpack/src/main/java/net/skyscanner/backpack/skeleton/BpkHeadlineSkeleton.kt +++ b/Backpack/src/main/java/net/skyscanner/backpack/skeleton/BpkHeadlineSkeleton.kt @@ -94,9 +94,9 @@ class BpkHeadlineSkeleton @JvmOverloads constructor( setMeasuredDimension(widthMeasureSpec, heightSize) } - override fun onDraw(canvas: Canvas?) { + override fun onDraw(canvas: Canvas) { val borderRadius = context.resources.getDimensionPixelSize(R.dimen.bpkBorderRadiusXs) - canvas?.drawRoundRect(0f, 0f, width.toFloat(), height.toFloat(), borderRadius.toFloat(), borderRadius.toFloat(), paint) + canvas.drawRoundRect(0f, 0f, width.toFloat(), height.toFloat(), borderRadius.toFloat(), borderRadius.toFloat(), paint) } private companion object { diff --git a/android-configuration.gradle b/android-configuration.gradle index 20b5bd9380..1f97ad6417 100644 --- a/android-configuration.gradle +++ b/android-configuration.gradle @@ -17,7 +17,7 @@ */ android { - compileSdk 33 + compileSdk 34 defaultConfig { minSdkVersion 28 diff --git a/app/screenshots/oss/debug/default/net.skyscanner.backpack.compose.select.BpkSelectTest_dropdownlist.png b/app/screenshots/oss/debug/default/net.skyscanner.backpack.compose.select.BpkSelectTest_dropdownlist.png index c3292ac7b8..d513f744f7 100644 Binary files a/app/screenshots/oss/debug/default/net.skyscanner.backpack.compose.select.BpkSelectTest_dropdownlist.png and b/app/screenshots/oss/debug/default/net.skyscanner.backpack.compose.select.BpkSelectTest_dropdownlist.png differ diff --git a/app/screenshots/oss/debug/dm/net.skyscanner.backpack.compose.select.BpkSelectTest_dropdownlist.png b/app/screenshots/oss/debug/dm/net.skyscanner.backpack.compose.select.BpkSelectTest_dropdownlist.png index 17d75342be..a17cfaa918 100644 Binary files a/app/screenshots/oss/debug/dm/net.skyscanner.backpack.compose.select.BpkSelectTest_dropdownlist.png and b/app/screenshots/oss/debug/dm/net.skyscanner.backpack.compose.select.BpkSelectTest_dropdownlist.png differ diff --git a/app/screenshots/oss/debug/rtl/net.skyscanner.backpack.compose.select.BpkSelectTest_dropdownlist.png b/app/screenshots/oss/debug/rtl/net.skyscanner.backpack.compose.select.BpkSelectTest_dropdownlist.png index f87709a4b1..b088034bd6 100644 Binary files a/app/screenshots/oss/debug/rtl/net.skyscanner.backpack.compose.select.BpkSelectTest_dropdownlist.png and b/app/screenshots/oss/debug/rtl/net.skyscanner.backpack.compose.select.BpkSelectTest_dropdownlist.png differ diff --git a/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/barchart/internal/BarChartTitle.kt b/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/barchart/internal/BarChartTitle.kt index 5d3cb1be84..35f6ab7329 100644 --- a/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/barchart/internal/BarChartTitle.kt +++ b/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/barchart/internal/BarChartTitle.kt @@ -19,7 +19,7 @@ package net.skyscanner.backpack.compose.barchart.internal import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ContentTransform import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.fadeIn @@ -62,14 +62,14 @@ internal fun BarChartTitle( when (scrollingDirection) { ScrollingDirection.Forward -> ContentTransform( - targetContentEnter = fadeIn() + slideIntoContainer(AnimatedContentScope.SlideDirection.Start), - initialContentExit = fadeOut() + slideOutOfContainer(AnimatedContentScope.SlideDirection.Start), + targetContentEnter = fadeIn() + slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start), + initialContentExit = fadeOut() + slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start), ) ScrollingDirection.Backward -> ContentTransform( - targetContentEnter = fadeIn() + slideIntoContainer(AnimatedContentScope.SlideDirection.End), - initialContentExit = fadeOut() + slideOutOfContainer(AnimatedContentScope.SlideDirection.End), + targetContentEnter = fadeIn() + slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End), + initialContentExit = fadeOut() + slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End), ) } }, diff --git a/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/carousel/BpkCarousel.kt b/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/carousel/BpkCarousel.kt index 595ac437c3..f105ad9af7 100644 --- a/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/carousel/BpkCarousel.kt +++ b/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/carousel/BpkCarousel.kt @@ -43,7 +43,6 @@ fun BpkCarousel( modifier = Modifier .testTag("pager") .fillMaxSize(), - pageCount = if (internalState.pageCount > 1) Int.MAX_VALUE else 1, // if count > 1, set to Int.MAX_VALUE for infinite looping state = internalState.delegate, ) { content(internalState.getModdedPageNumber(it, internalState.pageCount)) diff --git a/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/carousel/BpkCarouselState.kt b/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/carousel/BpkCarouselState.kt index efb565e019..f4be1a8550 100644 --- a/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/carousel/BpkCarouselState.kt +++ b/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/carousel/BpkCarouselState.kt @@ -24,9 +24,11 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.pager.PagerState -import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable sealed interface BpkCarouselState : ScrollableState { @@ -58,7 +60,14 @@ fun rememberBpkCarouselState( initialImage: Int = 0, ): BpkCarouselState { val initialPage = (Int.MAX_VALUE / 2) + initialImage - val pagerState = rememberPagerState(initialPage = initialPage) + + val pagerState = rememberSaveable(saver = InfinitePagerState.Saver) { + InfinitePagerState( + initialPage, + totalImages, + ) + } + return remember(pagerState, totalImages) { BpkCarouselInternalState(delegate = pagerState, totalImages = totalImages) } @@ -70,7 +79,10 @@ fun BpkCarouselState( initialImage: Int = 0, ): BpkCarouselState { val initialPage = (Int.MAX_VALUE / 2) + initialImage - return BpkCarouselInternalState(delegate = PagerState(initialPage = initialPage), totalImages = totalImages) + return BpkCarouselInternalState( + delegate = InfinitePagerState(initialPage = initialPage, totalPages = totalImages), + totalImages = totalImages, + ) } internal fun BpkCarouselState.asInternalState(): BpkCarouselInternalState = @@ -79,7 +91,7 @@ internal fun BpkCarouselState.asInternalState(): BpkCarouselInternalState = } @OptIn(ExperimentalFoundationApi::class) -internal class BpkCarouselInternalState constructor( +internal class BpkCarouselInternalState( val delegate: PagerState, val totalImages: Int, ) : BpkCarouselState, ScrollableState by delegate { @@ -113,3 +125,31 @@ internal class BpkCarouselInternalState constructor( else -> this - floorDiv(other) * other } } + +@OptIn(ExperimentalFoundationApi::class) +private class InfinitePagerState( + initialPage: Int, + private val totalPages: Int, +) : PagerState(initialPage, initialPageOffsetFraction = 0f) { + + override val pageCount: Int + // if count > 1, set to Int.MAX_VALUE for infinite looping + get() = if (totalPages > 1) Int.MAX_VALUE else 1 + + companion object { + val Saver: Saver = listSaver( + save = { + listOf( + it.currentPage, + it.totalPages, + ) + }, + restore = { + InfinitePagerState( + initialPage = it[0], + totalPages = it[1], + ) + }, + ) + } +} diff --git a/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/floatingnotification/internal/BpkFloatingNotification.kt b/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/floatingnotification/internal/BpkFloatingNotification.kt index 6d081eaeb3..86355bdb8e 100644 --- a/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/floatingnotification/internal/BpkFloatingNotification.kt +++ b/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/floatingnotification/internal/BpkFloatingNotification.kt @@ -18,7 +18,7 @@ package net.skyscanner.backpack.compose.floatingnotification.internal -import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ContentTransform import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.tween @@ -100,7 +100,7 @@ internal fun BpkFloatingNotificationImpl( @Composable internal fun floatingNotificationTransforms( slideDistancePx: Int, -): AnimatedContentScope.() -> ContentTransform = +): AnimatedContentTransitionScope.() -> ContentTransform = { ContentTransform( targetContentEnter = fadeIn(tween(TRANSITION_DURATION)) + slideInVertically(tween(TRANSITION_DURATION)) { slideDistancePx }, diff --git a/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/select/internal/BpkSelectImpl.kt b/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/select/internal/BpkSelectImpl.kt index a80bd21a35..35bce67eea 100644 --- a/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/select/internal/BpkSelectImpl.kt +++ b/backpack-compose/src/main/kotlin/net/skyscanner/backpack/compose/select/internal/BpkSelectImpl.kt @@ -55,7 +55,7 @@ internal fun BpkSelectImpl( onSelectionChange: ((selectedIndex: Int) -> Unit)? = null, ) { var expanded by remember { mutableStateOf(false) } - val selectText = if (selectedIndex != null && selectedIndex > -1 && options.size >= selectedIndex) options[selectedIndex] else "" + val selectText = selectedIndex?.let { options.getOrNull(selectedIndex) } ?: "" ExposedDropdownMenuBox( expanded = expanded, @@ -72,16 +72,27 @@ internal fun BpkSelectImpl( ) ExposedDropdownMenu( expanded = if (status != BpkFieldStatus.Disabled) expanded else false, - modifier = Modifier.background(BpkTheme.colors.surfaceDefault).fillMaxWidth(), + modifier = Modifier + .background(BpkTheme.colors.surfaceDefault) + .fillMaxWidth(), onDismissRequest = { expanded = false }, ) { options.forEachIndexed { index, option -> - val itemBackgroundColor = if (index == selectedIndex) BpkTheme.colors.surfaceHighlight else BpkTheme.colors.surfaceDefault + val itemBackgroundColor = + if (index == selectedIndex) BpkTheme.colors.surfaceHighlight else BpkTheme.colors.surfaceDefault DropdownMenuItem( modifier = Modifier .height(BpkSpacing.Lg.times(2)) .background(itemBackgroundColor), - text = { BpkText(text = option, color = BpkTheme.colors.textPrimary, maxLines = 1, overflow = TextOverflow.Ellipsis) }, + text = { + BpkText( + text = option, + color = BpkTheme.colors.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = BpkTheme.typography.label1, + ) + }, onClick = { expanded = false if (index != selectedIndex) { @@ -104,10 +115,12 @@ internal fun BpkSelectImpl( onClick: (() -> Unit)? = null, ) { BpkTextFieldImpl( - modifier = if (status == BpkFieldStatus.Disabled) modifier else modifier.clickable(bounded = true, role = Role.Button) { - onClick?.let { - it() - } }, + modifier = if (status == BpkFieldStatus.Disabled) modifier else modifier.clickable( + bounded = true, + role = Role.Button, + ) { + onClick?.let { it() } + }, value = text, onValueChange = {}, readOnly = true, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 28ca75664d..81194af2bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ plugin-nexusPublishing = "io.github.gradle-nexus:publish-plugin:1.3.0" plugin-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } plugin-ksp = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } -detektRules-compose = "io.nlopez.compose.rules:detekt:0.2.1" +detektRules-compose = "io.nlopez.compose.rules:detekt:0.2.2" detektRules-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detektRules-libraries = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" } @@ -62,7 +62,7 @@ test-compose = { group = "androidx.compose.ui", name = "ui-test-junit4" } google-material = "com.google.android.material:material:1.9.0" google-maps = "com.google.android.gms:play-services-maps:18.1.0" -google-mapsCompose = "com.google.maps.android:maps-compose:2.14.0" +google-mapsCompose = "com.google.maps.android:maps-compose:2.14.1" google-kspApi = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } google-guava = "com.google.guava:guava:32.1.2-jre" @@ -80,7 +80,7 @@ kotlin-compilerTestingKsp = { module = "com.github.tschuchortdev:kotlin-compile- lint-lint = { module = "com.android.tools.lint:lint", version.ref = "lint" } lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "lint" } -compose-bom = { group = "androidx.compose", name = "compose-bom", version = "2023.06.01" } +compose-bom = { group = "androidx.compose", name = "compose-bom", version = "2023.09.00" } compose-ui = { group = "androidx.compose.ui", name = "ui" } compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } diff --git a/package-lock.json b/package-lock.json index de7c5a883b..7ec5285964 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,9 +37,9 @@ } }, "node_modules/@skyscanner/bpk-svgs": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@skyscanner/bpk-svgs/-/bpk-svgs-18.1.0.tgz", - "integrity": "sha512-lgNvbEVrYgfbODmFcqaVYUAZV5Wnja5ZzVr5GrZenKfS1G7bpqKJekyGP4MNiL/A+7jQxK9W89N8fAPtijAJ+g==", + "version": "18.1.2", + "resolved": "https://registry.npmjs.org/@skyscanner/bpk-svgs/-/bpk-svgs-18.1.2.tgz", + "integrity": "sha512-DEhd+WfINmKpUEyP3o4tgrnDXzlqDhKXaL8L31//t3Uptb9dVzk7y0drm4afKoezXWJg3GM7sduP/QxR3sNUZw==", "dev": true }, "node_modules/color": { @@ -114,9 +114,9 @@ } }, "@skyscanner/bpk-svgs": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@skyscanner/bpk-svgs/-/bpk-svgs-18.1.0.tgz", - "integrity": "sha512-lgNvbEVrYgfbODmFcqaVYUAZV5Wnja5ZzVr5GrZenKfS1G7bpqKJekyGP4MNiL/A+7jQxK9W89N8fAPtijAJ+g==", + "version": "18.1.2", + "resolved": "https://registry.npmjs.org/@skyscanner/bpk-svgs/-/bpk-svgs-18.1.2.tgz", + "integrity": "sha512-DEhd+WfINmKpUEyP3o4tgrnDXzlqDhKXaL8L31//t3Uptb9dVzk7y0drm4afKoezXWJg3GM7sduP/QxR3sNUZw==", "dev": true }, "color": {