diff --git a/shared/src/commonMain/kotlin/com/airbnb/sample/data/explore/SearchLocation.kt b/shared/src/commonMain/kotlin/com/airbnb/sample/data/explore/SearchLocation.kt index 10a7d93..9aee8d6 100644 --- a/shared/src/commonMain/kotlin/com/airbnb/sample/data/explore/SearchLocation.kt +++ b/shared/src/commonMain/kotlin/com/airbnb/sample/data/explore/SearchLocation.kt @@ -4,6 +4,8 @@ package com.airbnb.sample.data.explore sealed interface SearchLocationOption { + val imageUrl: String? + companion object { val choices = listOf( Flexible, @@ -18,26 +20,46 @@ sealed interface SearchLocationOption { ) } - data object Flexible : SearchLocationOption + data object Flexible : SearchLocationOption { + override val imageUrl: String = "https://images.unsplash.com/photo-1613235795113-e2c223afc08c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8d29ybGQtbWFwfHx8fHx8MTcwMDYwMDYyOA&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080" + } - data class Custom(val place: String) : SearchLocationOption + data class Custom(val place: String) : SearchLocationOption { + override val imageUrl: String? = null + } sealed interface PhysicalPlace { - data object Europe : SearchLocationOption, PhysicalPlace - data object Canada : SearchLocationOption, PhysicalPlace - data object Caribbean : SearchLocationOption, PhysicalPlace - data object Italy : SearchLocationOption, PhysicalPlace - data object South_America : SearchLocationOption, PhysicalPlace - data object United_Kingdom : SearchLocationOption, PhysicalPlace - data object Mexico : SearchLocationOption, PhysicalPlace - data object Spain : SearchLocationOption, PhysicalPlace + data object Europe : SearchLocationOption, PhysicalPlace { + override val imageUrl: String = "https://images.unsplash.com/photo-1576185850227-1f72b7f8d483?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8ZXVyb3BlLW1hcHx8fHx8fDE3MDA2MDA3MTc&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080" + } + data object Canada : SearchLocationOption, PhysicalPlace { + override val imageUrl: String = "https://images.unsplash.com/photo-1578924825042-31d14cf13c35?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8Y2FuYWRhLW1hcHx8fHx8fDE3MDA2MDA3ODg&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080" + } + data object Caribbean : SearchLocationOption, PhysicalPlace { + override val imageUrl: String = "https://images.unsplash.com/photo-1625239622428-ba0ae330a1f9?q=80&w=2826&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" + } + data object Italy : SearchLocationOption, PhysicalPlace { + override val imageUrl: String = "https://images.unsplash.com/photo-1553290322-0440fe3b1ddd?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" + } + data object South_America : SearchLocationOption, PhysicalPlace { + override val imageUrl: String = "https://images.unsplash.com/photo-1544906243-a69767cc000b?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" + } + data object United_Kingdom : SearchLocationOption, PhysicalPlace { + override val imageUrl: String = "https://images.unsplash.com/photo-1662578546948-a30badf89995?q=80&w=2672&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" + } + data object Mexico : SearchLocationOption, PhysicalPlace { + override val imageUrl: String = "https://images.unsplash.com/photo-1553290322-0440fe3b1ddd?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" + } + data object Spain : SearchLocationOption, PhysicalPlace { + override val imageUrl: String = "https://images.unsplash.com/photo-1522072176817-41673f7f0ccc?q=80&w=2819&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" + } fun named() = this::class.simpleName.orEmpty().replace("_", " ") } fun displayLabel() = when (this) { - is SearchLocationOption.Custom -> place - is SearchLocationOption.PhysicalPlace -> named() - SearchLocationOption.Flexible -> "I'm flexible" + is Custom -> place + is PhysicalPlace -> named() + Flexible -> "I'm flexible" } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/airbnb/sample/screens/explore/search/components/LocationSelection.kt b/shared/src/commonMain/kotlin/com/airbnb/sample/screens/explore/search/components/LocationSelection.kt index 48bf36f..b09e98a 100644 --- a/shared/src/commonMain/kotlin/com/airbnb/sample/screens/explore/search/components/LocationSelection.kt +++ b/shared/src/commonMain/kotlin/com/airbnb/sample/screens/explore/search/components/LocationSelection.kt @@ -3,6 +3,7 @@ package com.airbnb.sample.screens.explore.search.components import androidx.compose.animation.Crossfade import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -22,6 +23,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.airbnb.sample.data.explore.SearchLocationOption @@ -29,6 +34,7 @@ import com.airbnb.sample.screens.explore.experience.components.SearchBar import com.airbnb.sample.theme.dimens import com.airbnb.sample.theme.secondaryText import com.airbnb.sample.utils.printed +import com.seiko.imageloader.rememberImagePainter @Composable internal fun LocationSelection( @@ -100,10 +106,28 @@ internal fun LocationSelection( modifier = Modifier .fillParentMaxWidth(0.3f) .aspectRatio(1f) + .clip(MaterialTheme.shapes.medium) .clickable { onLocationOptionSelected(choice) } .background(MaterialTheme.colorScheme.background) .border(borderWidth, borderColor, MaterialTheme.shapes.medium) - ) + ) { + val imageUrl = choice.imageUrl + if (imageUrl != null) { + val painter = + rememberImagePainter(url = imageUrl) + Image( + modifier = Modifier.matchParentSize(), + painter = painter, + contentDescription = null, + contentScale = ContentScale.FillBounds, + colorFilter = ColorFilter.colorMatrix( + ColorMatrix().apply { + setToSaturation(0f) + } + ) + ) + } + } Text( text = choice.displayLabel(), style = MaterialTheme.typography.bodySmall diff --git a/shared/src/commonMain/kotlin/com/airbnb/sample/screens/explore/search/components/ScrollingCalendar.kt b/shared/src/commonMain/kotlin/com/airbnb/sample/screens/explore/search/components/ScrollingCalendar.kt deleted file mode 100644 index 5097090..0000000 --- a/shared/src/commonMain/kotlin/com/airbnb/sample/screens/explore/search/components/ScrollingCalendar.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.airbnb.sample.screens.explore.search.components - diff --git a/shared/src/commonMain/kotlin/com/airbnb/sample/ui/components/ScrollingCalendar.kt b/shared/src/commonMain/kotlin/com/airbnb/sample/ui/components/ScrollingCalendar.kt index 28066ae..cf38e4a 100644 --- a/shared/src/commonMain/kotlin/com/airbnb/sample/ui/components/ScrollingCalendar.kt +++ b/shared/src/commonMain/kotlin/com/airbnb/sample/ui/components/ScrollingCalendar.kt @@ -1,8 +1,10 @@ package com.airbnb.sample.ui.components import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background import androidx.compose.foundation.clickable 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.fillMaxSize @@ -20,11 +22,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.CompositingStrategy import androidx.compose.ui.graphics.Path @@ -36,11 +40,14 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import com.airbnb.sample.theme.dimens import com.airbnb.sample.theme.secondaryText +import com.airbnb.sample.utils.ui.addIf import com.kizitonwose.calendar.compose.VerticalCalendar import com.kizitonwose.calendar.compose.rememberCalendarState import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.DayPosition import com.kizitonwose.calendar.core.YearMonth +import com.kizitonwose.calendar.core.atEndOfMonth +import com.kizitonwose.calendar.core.atStartOfMonth import com.kizitonwose.calendar.core.dayOfWeekRangeFrom import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale import com.kizitonwose.calendar.core.lastDayOfWeekFromLocale @@ -89,6 +96,13 @@ fun VerticalScrollingCalendar( ) }, dayContent = { day -> + val startOfMonth = YearMonth.of(day.date) + .plusMonths(1) + .atStartOfMonth() + val endOfMonth = YearMonth.of(day.date) + .minusMonths(1) + .atEndOfMonth() + // only render days in the given month (no in or out dates) if (day.position == DayPosition.MonthDate) { val isTodayOrFuture = day.date.toEpochDays() >= today.toEpochDays() @@ -114,6 +128,31 @@ fun VerticalScrollingCalendar( color = if (isTodayOrFuture) textColor else MaterialTheme.colorScheme.secondaryText, textDecoration = if (!isTodayOrFuture) TextDecoration.LineThrough else null ) + } else { + val range = if (selectedDates.count() > 1) { + selectedDates.first().rangeTo(selectedDates.last()) + } else { + null + } + + if (range != null && range.contains(day.date)) { + val month = YearMonth.of(day.date) + if (day.date.dayOfMonth == 1 || day.date.dayOfMonth == month.lengthOfMonth()) { + Text( + modifier = Modifier + .fillMaxSize() + .align(Alignment.Center) + .graphicsLayer { + compositingStrategy = CompositingStrategy.Offscreen + } + .drawCrossMonthGradient(day) + .padding(MaterialTheme.dimens.staticGrid.x3), + text = day.date.dayOfMonth.toString(), // not drawn in [drawCrossMonthGradient] + style = MaterialTheme.typography.labelSmall, + textAlign = TextAlign.Center, + ) + } + } } } ) @@ -153,6 +192,7 @@ private fun Modifier.drawSelectionsAndRange( val shape = MaterialTheme.shapes.medium val density = LocalDensity.current + this.drawBehind { val range = if (selectedDates.count() > 1) { selectedDates.first().rangeTo(selectedDates.last()) @@ -180,7 +220,7 @@ private fun Modifier.drawSelectionsAndRange( RoundRect( rect = Rect( offset = Offset(xMax - curveSize + 1, yMin), - size = Size(curveSize, yMax + radiusDiff), + size = Size(curveSize, yMax + radiusDiff - 10f), ), topLeft = cornerRadius, bottomLeft = cornerRadius @@ -197,7 +237,7 @@ private fun Modifier.drawSelectionsAndRange( RoundRect( rect = Rect( offset = Offset(0f, yMin), - size = Size(curveSize, yMax + radiusDiff) + size = Size(curveSize, yMax + radiusDiff - 10f) ), topRight = cornerRadius, bottomRight = cornerRadius @@ -217,7 +257,10 @@ private fun Modifier.drawSelectionsAndRange( RoundRect( rect = Rect( offset = Offset(0f, 0f), - size = size, + size = Size( + width = size.width, + height = size.height - 10f + ), ), topLeft = cornerRadius, bottomLeft = cornerRadius @@ -227,6 +270,7 @@ private fun Modifier.drawSelectionsAndRange( drawPath(path, backgroundColor) } + lastDayOfWeekFromLocale() -> { // end of week, round end val path = Path().apply { @@ -234,7 +278,10 @@ private fun Modifier.drawSelectionsAndRange( RoundRect( rect = Rect( offset = Offset(0f, 0f), - size = size, + size = Size( + width = size.width, + height = size.height - 10f + ), ), topRight = cornerRadius, bottomRight = cornerRadius @@ -244,8 +291,12 @@ private fun Modifier.drawSelectionsAndRange( drawPath(path, backgroundColor) } + else -> { - drawRect(backgroundColor) + drawRect( + color = backgroundColor, + size = Size(width = size.width, height = size.height - 10f), + ) } } } @@ -256,4 +307,30 @@ private fun Modifier.drawSelectionsAndRange( drawCircle(Color.Black) } } +} + +private fun Modifier.drawCrossMonthGradient(day: CalendarDay) = composed { + this.drawWithContent { + if (day.position == DayPosition.OutDate) { + drawRect( + brush = Brush.horizontalGradient( + colors = listOf( + Color(0xFFF1F1F1), + Color.Transparent, + ) + ), + size = Size(width = size.width, height = size.height - 10f), + ) + } else if (day.position == DayPosition.InDate) { + drawRect( + brush = Brush.horizontalGradient( + colors = listOf( + Color.Transparent, + Color(0xFFF1F1F1), + ) + ), + size = Size(width = size.width, height = size.height - 10f), + ) + } + } } \ No newline at end of file