Skip to content

Instantly share code, notes, and snippets.

@SatyaSnehith
Forked from bagus2x/TwitterDatePicker.kt
Last active May 22, 2024 16:00
Show Gist options
  • Save SatyaSnehith/ee599bb3bc91d0e717c442f72c59f882 to your computer and use it in GitHub Desktop.
Save SatyaSnehith/ee599bb3bc91d0e717c442f72c59f882 to your computer and use it in GitHub Desktop.
Jetpack compose twitter date picker (dob). Jetpack Compose scrollable date picker.
package com.satyasnehith.wpd
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.collectLatest
import java.time.LocalDate
@OptIn(ExperimentalTextApi::class, ExperimentalFoundationApi::class)
@Composable
fun DatePicker(
modifier: Modifier = Modifier,
state: DatePickerState = rememberDatePickerState(),
style: TextStyle = MaterialTheme.typography.bodyMedium,
onChange: (LocalDate) -> Unit,
) {
val textMeasurer = rememberTextMeasurer()
val textLayoutResult = textMeasurer.measure(text = "99999", style = style)
val density = LocalDensity.current
val pageSize = with(density) {
DpSize(
width = textLayoutResult.size.width.toDp() + 32.dp,
height = textLayoutResult.size.height.toDp() + 32.dp
)
}
val pagerSize = pageSize * 3
val datesPagerState = rememberPagerState(
initialPage = state.initialDateIndex,
pageCount = {
state.dates.size
}
)
val monthsPagerState = rememberPagerState(
initialPage = state.initialMonthIndex,
pageCount = {
state.months.size
}
)
val yearsPagerState = rememberPagerState(
initialPage = state.initialYearIndex,
pageCount = {
state.years.size
}
)
LaunchedEffect(Unit) {
snapshotFlow { yearsPagerState.currentPage }.collectLatest { index ->
val year = state.years[index]
val date = state.dates[datesPagerState.currentPage].withYear(year)
val dateIndex = state.dates.indexOf(date)
val monthsIndex = state.months.indexOf(date.month to date.year)
datesPagerState.scrollToPage(dateIndex)
monthsPagerState.scrollToPage(monthsIndex)
}
}
LaunchedEffect(Unit) {
snapshotFlow { monthsPagerState.currentPage }.collectLatest { index ->
val (month, year) = state.months[index]
val yearIndex = state.years.indexOf(year)
val date = state.dates[datesPagerState.currentPage]
.withMonth(month.value)
.withYear(year)
val dateIndex = state.dates.indexOf(date)
yearsPagerState.scrollToPage(yearIndex)
datesPagerState.scrollToPage(dateIndex)
}
}
LaunchedEffect(Unit) {
snapshotFlow { datesPagerState.currentPage }.collectLatest { index ->
val date = state.dates[index].apply(onChange)
val yearIndex = state.years.indexOf(date.year)
val monthIndex = state.months.indexOf(date.month to date.year)
yearsPagerState.scrollToPage(yearIndex)
monthsPagerState.scrollToPage(monthIndex)
}
}
val indicator = Modifier.indicator(
color = MaterialTheme.colorScheme.onSurface,
pageHeight = pageSize.height
)
Row(
modifier = modifier
.size(pagerSize)
.fadeTopAndBottom()
) {
VerticalPager(
contentPadding = PaddingValues(vertical = pageSize.height),
state = datesPagerState,
modifier = indicator
) { index ->
val date = state.dates[index]
Text(
text = "${date.dayOfMonth}",
modifier = Modifier
.size(pageSize)
.wrapContentSize(align = Alignment.Center)
)
}
VerticalPager(
contentPadding = PaddingValues(vertical = pageSize.height),
state = monthsPagerState,
modifier = indicator
) { index ->
val (month, _) = state.months[index]
Text(
text = "${month.value}",
modifier = Modifier
.size(pageSize)
.wrapContentSize(align = Alignment.Center)
)
}
VerticalPager(
contentPadding = PaddingValues(vertical = pageSize.height),
state = yearsPagerState,
modifier = indicator
) { index ->
val year = state.years[index]
Text(
text = "$year",
modifier = Modifier
.size(pageSize)
.wrapContentSize(align = Alignment.Center)
)
}
}
}
@Preview(showSystemUi = false, showBackground = true)
@Composable
private fun DatePickerPreview() {
DatePicker(
onChange = {
}
)
}
@Stable
class DatePickerState(
currentDate: LocalDate,
startDate: LocalDate,
endDate: LocalDate
) {
val years = (startDate.year..endDate.year).toList()
val dates = (startDate.toEpochDay()..endDate.toEpochDay()).map(LocalDate::ofEpochDay)
val months = dates
.groupBy { it.month to it.year }
.toList()
.map { it.first }
.sortedBy { it.second }
val initialYearIndex = years.indexOf(currentDate.year)
val initialMonthIndex = months.indexOf(currentDate.month to currentDate.year)
val initialDateIndex = dates.indexOf(currentDate)
}
@Composable
fun rememberDatePickerState(
currentDate: LocalDate = LocalDate.now(),
startDate: LocalDate = LocalDate.now().minusYears(100),
endDate: LocalDate = LocalDate.now().plusYears(100)
): DatePickerState {
return DatePickerState(currentDate, startDate, endDate)
}
private fun Modifier.indicator(
color: Color,
strokeWidth: Dp = 2.dp,
pageHeight: Dp
): Modifier {
return this.drawBehind {
drawLine(
color = color,
strokeWidth = strokeWidth.toPx(),
start = Offset(
x = 16.dp.toPx(),
y = pageHeight.toPx()
),
end = Offset(
x = size.width - 16.dp.toPx(),
y = pageHeight.toPx()
),
cap = StrokeCap.Round
)
drawLine(
color = color,
strokeWidth = strokeWidth.toPx(),
start = Offset(
x = 16.dp.toPx(),
y = pageHeight.toPx() * 2 - strokeWidth.toPx()
),
end = Offset(
x = size.width - 16.dp.toPx(),
y = pageHeight.toPx() * 2 - strokeWidth.toPx()
),
cap = StrokeCap.Round
)
}
}
private fun Modifier.fadeTopAndBottom(): Modifier {
return this
.graphicsLayer { alpha = 0.99f }
.drawWithContent {
drawContent()
drawRect(
brush = Brush.verticalGradient(
colors = listOf(Color.Transparent, Color.Black, Color.Black),
),
blendMode = BlendMode.DstIn,
topLeft = Offset.Zero,
size = Size(
width = size.width,
height = size.height / 3
)
)
this.rotate(180f) {
drawRect(
brush = Brush.verticalGradient(
colors = listOf(Color.Transparent, Color.Black, Color.Black),
),
blendMode = BlendMode.DstIn,
topLeft = Offset.Zero,
size = Size(
width = size.width,
height = size.height / 3
)
)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment