Rework on the wheel picker (#8559)

* Rework the wheel picker

doesn't need for the animation to stop to change the value

* fix

---------

Co-authored-by: arkon <arkon@users.noreply.github.com>
This commit is contained in:
Ivan Iskandar 2023-02-13 11:10:47 +07:00 committed by GitHub
parent 2970eca9e4
commit be4072c86b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 322 additions and 60 deletions

View File

@ -6,7 +6,6 @@
"ignoreDeps": [
"androidx.core:core-splashscreen",
"com.android.tools:r8",
"com.google.guava:guava",
"com.github.commandiron:WheelPickerCompose"
"com.google.guava:guava"
]
}

View File

@ -241,7 +241,6 @@ dependencies {
implementation(libs.aboutLibraries.compose)
implementation(libs.cascade)
implementation(libs.bundles.voyager)
implementation(libs.wheelpicker)
implementation(libs.materialmotion.core)
// Logging

View File

@ -0,0 +1,286 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.util.padding
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import java.text.DateFormatSymbols
import java.time.LocalDate
import kotlin.math.absoluteValue
@Composable
fun WheelPicker(
modifier: Modifier = Modifier,
startIndex: Int = 0,
count: Int,
size: DpSize = DpSize(128.dp, 128.dp),
onSelectionChanged: (index: Int) -> Unit = {},
backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
WheelPickerDefaults.Background(size = it)
},
itemContent: @Composable LazyItemScope.(index: Int) -> Unit,
) {
val lazyListState = rememberLazyListState(startIndex)
LaunchedEffect(lazyListState, onSelectionChanged) {
snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
.map { calculateSnappedItemIndex(lazyListState) }
.distinctUntilChanged()
.collectLatest {
onSelectionChanged(it)
}
}
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
backgroundContent?.invoke(size)
LazyColumn(
modifier = Modifier
.height(size.height)
.width(size.width),
state = lazyListState,
contentPadding = PaddingValues(vertical = size.height / RowCount * ((RowCount - 1) / 2)),
flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState),
) {
items(count) { index ->
Box(
modifier = Modifier
.height(size.height / RowCount)
.width(size.width)
.alpha(
calculateAnimatedAlpha(
lazyListState = lazyListState,
index = index,
),
),
contentAlignment = Alignment.Center,
) {
itemContent(index)
}
}
}
}
}
@Composable
fun WheelTextPicker(
modifier: Modifier = Modifier,
startIndex: Int = 0,
texts: List<String>,
size: DpSize = DpSize(128.dp, 128.dp),
onSelectionChanged: (index: Int) -> Unit = {},
backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
WheelPickerDefaults.Background(size = it)
},
) {
WheelPicker(
modifier = modifier,
startIndex = startIndex,
count = remember(texts) { texts.size },
size = size,
onSelectionChanged = onSelectionChanged,
backgroundContent = backgroundContent,
) {
WheelPickerDefaults.Item(text = texts[it])
}
}
@Composable
fun WheelDatePicker(
modifier: Modifier = Modifier,
startDate: LocalDate = LocalDate.now(),
minDate: LocalDate? = null,
maxDate: LocalDate? = null,
size: DpSize = DpSize(256.dp, 128.dp),
backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
WheelPickerDefaults.Background(size = it)
},
onSelectionChanged: (date: LocalDate) -> Unit = {},
) {
var internalSelection by remember { mutableStateOf(startDate) }
val internalOnSelectionChange: (LocalDate) -> Unit = {
internalSelection = it
onSelectionChanged(internalSelection)
}
Box(modifier = modifier, contentAlignment = Alignment.Center) {
backgroundContent?.invoke(size)
Row {
val singularPickerSize = DpSize(
width = size.width / 3,
height = size.height,
)
// Day
val dayOfMonths = remember(internalSelection, minDate, maxDate) {
if (minDate == null && maxDate == null) {
1..internalSelection.lengthOfMonth()
} else {
val minDay = if (minDate?.month == internalSelection.month &&
minDate?.year == internalSelection.year
) {
minDate.dayOfMonth
} else {
1
}
val maxDay = if (maxDate?.month == internalSelection.month &&
maxDate?.year == internalSelection.year
) {
maxDate.dayOfMonth
} else {
31
}
minDay..maxDay.coerceAtMost(internalSelection.lengthOfMonth())
}.toList()
}
WheelTextPicker(
size = singularPickerSize,
texts = dayOfMonths.map { it.toString() },
backgroundContent = null,
startIndex = dayOfMonths.indexOfFirst { it == startDate.dayOfMonth }.coerceAtLeast(0),
onSelectionChanged = { index ->
val newDayOfMonth = dayOfMonths[index]
internalOnSelectionChange(internalSelection.withDayOfMonth(newDayOfMonth))
},
)
// Month
val months = remember(internalSelection, minDate, maxDate) {
val monthRange = if (minDate == null && maxDate == null) {
1..12
} else {
val minMonth = if (minDate?.year == internalSelection.year) {
minDate.monthValue
} else {
1
}
val maxMonth = if (maxDate?.year == internalSelection.year) {
maxDate.monthValue
} else {
12
}
minMonth..maxMonth
}
val dateFormatSymbols = DateFormatSymbols()
monthRange.map { it to dateFormatSymbols.months[it - 1] }
}
WheelTextPicker(
size = singularPickerSize,
texts = months.map { it.second },
backgroundContent = null,
startIndex = months.indexOfFirst { it.first == startDate.monthValue }.coerceAtLeast(0),
onSelectionChanged = { index ->
val newMonth = months[index].first
internalOnSelectionChange(internalSelection.withMonth(newMonth))
},
)
// Year
val years = remember(minDate, maxDate) {
val minYear = minDate?.year?.coerceAtLeast(1900) ?: 1900
val maxYear = maxDate?.year?.coerceAtMost(2100) ?: 2100
val yearRange = minYear..maxYear
yearRange.toList()
}
WheelTextPicker(
size = singularPickerSize,
texts = years.map { it.toString() },
backgroundContent = null,
startIndex = years.indexOfFirst { it == startDate.year }.coerceAtLeast(0),
onSelectionChanged = { index ->
val newYear = years[index]
internalOnSelectionChange(internalSelection.withYear(newYear))
},
)
}
}
}
private fun LazyListState.snapOffsetForItem(itemInfo: LazyListItemInfo): Int {
val startScrollOffset = 0
val endScrollOffset = layoutInfo.let { it.viewportEndOffset - it.afterContentPadding }
return startScrollOffset + (endScrollOffset - startScrollOffset - itemInfo.size) / 2
}
private fun LazyListState.distanceToSnapForIndex(index: Int): Int {
val itemInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }
if (itemInfo != null) {
return itemInfo.offset - snapOffsetForItem(itemInfo)
}
return 0
}
private fun calculateAnimatedAlpha(
lazyListState: LazyListState,
index: Int,
): Float {
val distanceToIndexSnap = lazyListState.distanceToSnapForIndex(index).absoluteValue
val viewPortHeight = lazyListState.layoutInfo.viewportSize.height.toFloat()
val singleViewPortHeight = viewPortHeight / RowCount
return if (distanceToIndexSnap in 0..singleViewPortHeight.toInt()) {
1.2f - (distanceToIndexSnap / singleViewPortHeight)
} else {
0.2f
}
}
private fun calculateSnappedItemIndex(lazyListState: LazyListState): Int {
return lazyListState.layoutInfo.visibleItemsInfo
.maxBy { calculateAnimatedAlpha(lazyListState, it.index) }
.index
}
object WheelPickerDefaults {
@Composable
fun Background(size: DpSize) {
androidx.compose.material3.Surface(
modifier = Modifier
.size(size.width, size.height / RowCount),
shape = RoundedCornerShape(MaterialTheme.padding.medium),
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary),
content = {},
)
}
@Composable
fun Item(text: String) {
Text(
text = text,
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
)
}
}
private const val RowCount = 3

View File

@ -29,11 +29,11 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.commandiron.wheel_picker_compose.WheelDatePicker
import com.commandiron.wheel_picker_compose.WheelTextPicker
import eu.kanade.presentation.components.AlertDialogContent
import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.components.WheelDatePicker
import eu.kanade.presentation.components.WheelTextPicker
import eu.kanade.presentation.util.isScrolledToEnd
import eu.kanade.presentation.util.isScrolledToStart
import eu.kanade.presentation.util.minimumTouchTargetSize
@ -103,12 +103,9 @@ fun TrackChapterSelector(
content = {
WheelTextPicker(
modifier = Modifier.align(Alignment.Center),
texts = range.map { "$it" },
onScrollFinished = {
onSelectionChange(it)
null
},
startIndex = selection,
texts = range.map { "$it" },
onSelectionChanged = { onSelectionChange(it) },
)
},
onConfirm = onConfirm,
@ -129,12 +126,9 @@ fun TrackScoreSelector(
content = {
WheelTextPicker(
modifier = Modifier.align(Alignment.Center),
texts = selections,
onScrollFinished = {
onSelectionChange(selections[it])
null
},
startIndex = selections.indexOf(selection).coerceAtLeast(0),
texts = selections,
onSelectionChanged = { onSelectionChange(selections[it]) },
)
},
onConfirm = onConfirm,
@ -145,6 +139,8 @@ fun TrackScoreSelector(
@Composable
fun TrackDateSelector(
title: String,
minDate: LocalDate?,
maxDate: LocalDate?,
selection: LocalDate,
onSelectionChange: (LocalDate) -> Unit,
onConfirm: () -> Unit,
@ -170,7 +166,9 @@ fun TrackDateSelector(
)
WheelDatePicker(
startDate = selection,
onScrollFinished = {
minDate = minDate,
maxDate = maxDate,
onSelectionChanged = {
internalSelection = it
onSelectionChange(it)
},

View File

@ -1,15 +1,12 @@
package eu.kanade.presentation.more.settings.screen
import androidx.annotation.StringRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@ -23,7 +20,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
@ -35,10 +31,11 @@ import androidx.core.content.ContextCompat
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.commandiron.wheel_picker_compose.WheelPicker
import eu.kanade.domain.category.interactor.ResetCategoryFlags
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.presentation.category.visualName
import eu.kanade.presentation.components.WheelPicker
import eu.kanade.presentation.components.WheelPickerDefaults
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.widget.TriStateListDialog
import eu.kanade.presentation.util.collectAsState
@ -337,12 +334,7 @@ object SettingsLibraryScreen : SearchableSettings {
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Surface(
modifier = Modifier.size(maxWidth, maxHeight / 3),
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary),
) {}
WheelPickerDefaults.Background(size = DpSize(maxWidth, maxHeight))
val size = DpSize(width = maxWidth / 2, height = 128.dp)
Row {
@ -350,48 +342,24 @@ object SettingsLibraryScreen : SearchableSettings {
size = size,
count = 11,
startIndex = portraitValue,
onScrollFinished = {
onPortraitChange(it)
null
},
) { index, snappedIndex ->
ColumnPickerLabel(index = index, snappedIndex = snappedIndex)
onSelectionChanged = onPortraitChange,
backgroundContent = null,
) { index ->
WheelPickerDefaults.Item(text = getColumnValue(value = index))
}
WheelPicker(
size = size,
count = 11,
startIndex = landscapeValue,
onScrollFinished = {
onLandscapeChange(it)
null
},
) { index, snappedIndex ->
ColumnPickerLabel(index = index, snappedIndex = snappedIndex)
onSelectionChanged = onLandscapeChange,
backgroundContent = null,
) { index ->
WheelPickerDefaults.Item(text = getColumnValue(value = index))
}
}
}
}
@Composable
private fun ColumnPickerLabel(
index: Int,
snappedIndex: Int,
) {
Text(
modifier = Modifier.alpha(
when (snappedIndex) {
index + 1 -> 0.2f
index -> 1f
index - 1 -> 0.2f
else -> 0.2f
},
),
text = getColumnValue(index),
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
)
}
@Composable
@ReadOnlyComposable
private fun getColumnValue(value: Int): String {

View File

@ -445,6 +445,19 @@ private data class TrackDateSelectorScreen(
} else {
stringResource(R.string.track_finished_reading_date)
},
minDate = if (!start && track.started_reading_date > 0) {
// Disallow end date to be set earlier than start date
Instant.ofEpochMilli(track.started_reading_date).atZone(ZoneId.systemDefault()).toLocalDate()
} else {
null
},
maxDate = if (start && track.finished_reading_date > 0) {
// Disallow start date to be set later than finish date
Instant.ofEpochMilli(track.finished_reading_date).atZone(ZoneId.systemDefault()).toLocalDate()
} else {
// Disallow future dates
LocalDate.now()
},
selection = state.selection,
onSelectionChange = sm::setSelection,
onConfirm = { sm.setDate(); navigator.pop() },

View File

@ -61,7 +61,6 @@ photoview = "com.github.chrisbanes:PhotoView:2.3.0"
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
cascade = "me.saket.cascade:cascade-compose:2.0.0-rc01"
wheelpicker = "com.github.commandiron:WheelPickerCompose:1.0.11"
materialmotion-core = "io.github.fornewid:material-motion-compose-core:0.10.4"
logcat = "com.squareup.logcat:logcat:0.1"