mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-10 06:17:48 +01:00
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:
parent
2970eca9e4
commit
be4072c86b
7 changed files with 322 additions and 60 deletions
3
.github/renovate.json
vendored
3
.github/renovate.json
vendored
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -241,7 +241,6 @@ dependencies {
|
|||
implementation(libs.aboutLibraries.compose)
|
||||
implementation(libs.cascade)
|
||||
implementation(libs.bundles.voyager)
|
||||
implementation(libs.wheelpicker)
|
||||
implementation(libs.materialmotion.core)
|
||||
|
||||
// Logging
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() },
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue