Explorar o código

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 <[email protected]>
Ivan Iskandar %!s(int64=2) %!d(string=hai) anos
pai
achega
be4072c86b

+ 1 - 2
.github/renovate.json

@@ -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"
   ]
 }

+ 0 - 1
app/build.gradle.kts

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

+ 286 - 0
app/src/main/java/eu/kanade/presentation/components/WheelPicker.kt

@@ -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

+ 11 - 13
app/src/main/java/eu/kanade/presentation/manga/TrackInfoDialogSelector.kt

@@ -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)
                     },

+ 11 - 43
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt

@@ -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 {

+ 13 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackInfoDialog.kt

@@ -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() },

+ 0 - 1
gradle/libs.versions.toml

@@ -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"