From 60d86508600736099f389035d3ebef591a36fd14 Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sat, 15 Apr 2023 20:26:33 +0700 Subject: [PATCH] WheelPicker: Add manual input (#9338) --- .../presentation/components/AdaptiveSheet.kt | 1 + .../settings/screen/SettingsLibraryScreen.kt | 25 +- .../track/TrackInfoDialogSelector.kt | 7 +- .../core/components/WheelPicker.kt | 298 ++++++++---------- .../presentation/core/util/Modifier.kt | 5 +- 5 files changed, 159 insertions(+), 177 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt b/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt index e2bee6fde8..50ec9baecc 100644 --- a/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt +++ b/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt @@ -84,6 +84,7 @@ fun AdaptiveSheet( onDismissRequest = onDismissRequest, properties = DialogProperties( usePlatformDefaultWidth = false, + decorFitsSystemWindows = false, ), ) { AdaptiveSheetImpl( diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt index cc7277e3ce..6243a6570f 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt @@ -52,8 +52,8 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_UNREAD import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ -import tachiyomi.presentation.core.components.WheelPicker import tachiyomi.presentation.core.components.WheelPickerDefaults +import tachiyomi.presentation.core.components.WheelTextPicker import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -334,28 +334,25 @@ object SettingsLibraryScreen : SearchableSettings { modifier = modifier, contentAlignment = Alignment.Center, ) { - WheelPickerDefaults.Background(size = DpSize(maxWidth, maxHeight)) + WheelPickerDefaults.Background(size = DpSize(maxWidth, 128.dp)) val size = DpSize(width = maxWidth / 2, height = 128.dp) Row { - WheelPicker( - size = size, - count = 11, + val columns = (0..10).map { getColumnValue(value = it) } + WheelTextPicker( startIndex = portraitValue, + items = columns, + size = size, onSelectionChanged = onPortraitChange, backgroundContent = null, - ) { index -> - WheelPickerDefaults.Item(text = getColumnValue(value = index)) - } - WheelPicker( - size = size, - count = 11, + ) + WheelTextPicker( startIndex = landscapeValue, + items = columns, + size = size, onSelectionChanged = onLandscapeChange, backgroundContent = null, - ) { index -> - WheelPickerDefaults.Item(text = getColumnValue(value = index)) - } + ) } } } diff --git a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt index 6519b14c55..0822247a9d 100644 --- a/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt +++ b/app/src/main/java/eu/kanade/presentation/track/TrackInfoDialogSelector.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.R import tachiyomi.presentation.core.components.ScrollbarLazyColumn +import tachiyomi.presentation.core.components.WheelNumberPicker import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.components.material.AlertDialogContent import tachiyomi.presentation.core.components.material.Divider @@ -96,10 +97,10 @@ fun TrackChapterSelector( BaseSelector( title = stringResource(R.string.chapters), content = { - WheelTextPicker( + WheelNumberPicker( modifier = Modifier.align(Alignment.Center), startIndex = selection, - texts = range.map { "$it" }, + items = range.toList(), onSelectionChanged = { onSelectionChange(it) }, ) }, @@ -122,7 +123,7 @@ fun TrackScoreSelector( WheelTextPicker( modifier = Modifier.align(Alignment.Center), startIndex = selections.indexOf(selection).coerceAtLeast(0), - texts = selections, + items = selections, onSelectionChanged = { onSelectionChange(selections[it]) }, ) }, diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/WheelPicker.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/WheelPicker.kt index 2bb7d6d227..e244d08cc3 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/WheelPicker.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/WheelPicker.kt @@ -4,15 +4,17 @@ 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.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -20,89 +22,39 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope 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.graphics.SolidColor import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import tachiyomi.presentation.core.components.material.padding -import java.text.DateFormatSymbols -import java.time.LocalDate +import tachiyomi.presentation.core.util.clearFocusOnSoftKeyboardHide +import tachiyomi.presentation.core.util.clickableNoIndication +import tachiyomi.presentation.core.util.showSoftKeyboard import kotlin.math.absoluteValue @Composable -fun WheelPicker( +fun WheelNumberPicker( 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) - val haptic = LocalHapticFeedback.current - - LaunchedEffect(lazyListState, onSelectionChanged) { - snapshotFlow { lazyListState.firstVisibleItemScrollOffset } - .map { calculateSnappedItemIndex(lazyListState) } - .distinctUntilChanged() - .drop(1) - .collectLatest { - haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) - 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, + items: List, size: DpSize = DpSize(128.dp, 128.dp), onSelectionChanged: (index: Int) -> Unit = {}, backgroundContent: (@Composable (size: DpSize) -> Unit)? = { @@ -112,122 +64,150 @@ fun WheelTextPicker( WheelPicker( modifier = modifier, startIndex = startIndex, - count = remember(texts) { texts.size }, + items = items, size = size, onSelectionChanged = onSelectionChanged, + manualInputType = KeyboardType.Number, backgroundContent = backgroundContent, ) { - WheelPickerDefaults.Item(text = texts[it]) + WheelPickerDefaults.Item(text = "$it") } } @Composable -fun WheelDatePicker( +fun WheelTextPicker( modifier: Modifier = Modifier, - startDate: LocalDate = LocalDate.now(), - minDate: LocalDate? = null, - maxDate: LocalDate? = null, - size: DpSize = DpSize(256.dp, 128.dp), + startIndex: Int = 0, + items: List, + size: DpSize = DpSize(128.dp, 128.dp), + onSelectionChanged: (index: Int) -> Unit = {}, 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) + WheelPicker( + modifier = modifier, + startIndex = startIndex, + items = items, + size = size, + onSelectionChanged = onSelectionChanged, + backgroundContent = backgroundContent, + ) { + WheelPickerDefaults.Item(text = it) + } +} + +@Composable +private fun WheelPicker( + modifier: Modifier = Modifier, + startIndex: Int = 0, + items: List, + size: DpSize = DpSize(128.dp, 128.dp), + onSelectionChanged: (index: Int) -> Unit = {}, + manualInputType: KeyboardType? = null, + backgroundContent: (@Composable (size: DpSize) -> Unit)? = { + WheelPickerDefaults.Background(size = it) + }, + itemContent: @Composable LazyItemScope.(item: T) -> Unit, +) { + val haptic = LocalHapticFeedback.current + val lazyListState = rememberLazyListState(startIndex) + + var internalIndex by remember { mutableStateOf(startIndex) } + val internalOnSelectionChanged: (Int) -> Unit = { + internalIndex = it + onSelectionChanged(it) } - Box(modifier = modifier, contentAlignment = Alignment.Center) { + LaunchedEffect(lazyListState, onSelectionChanged) { + snapshotFlow { lazyListState.firstVisibleItemScrollOffset } + .map { calculateSnappedItemIndex(lazyListState) } + .distinctUntilChanged() + .drop(1) + .collectLatest { + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + internalOnSelectionChanged(it) + } + } + + Box( + modifier = modifier + .height(size.height) + .width(size.width), + 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() + var showManualInput by remember { mutableStateOf(false) } + if (showManualInput) { + var value by remember { + val currentString = items[internalIndex].toString() + mutableStateOf(TextFieldValue(text = currentString, selection = TextRange(currentString.length))) } - 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 scope = rememberCoroutineScope() + BasicTextField( + modifier = Modifier + .align(Alignment.Center) + .showSoftKeyboard(true) + .clearFocusOnSoftKeyboardHide { + scope.launch { + items + .indexOfFirst { it.toString() == value.text } + .takeIf { it >= 0 } + ?.apply { + internalOnSelectionChanged(this) + lazyListState.scrollToItem(this) + } + + showManualInput = false + } + }, + value = value, + onValueChange = { value = it }, + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = manualInputType!!, + imeAction = ImeAction.Done, + ), + textStyle = MaterialTheme.typography.titleMedium + + TextStyle( + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + ) + } else { + LazyColumn( + modifier = Modifier + .let { + if (manualInputType != null) { + it.clickableNoIndication { showManualInput = true } + } else { + it + } + }, + state = lazyListState, + contentPadding = PaddingValues(vertical = size.height / RowCount * ((RowCount - 1) / 2)), + flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState), + ) { + itemsIndexed(items) { index, item -> + Box( + modifier = Modifier + .height(size.height / RowCount) + .width(size.width) + .alpha( + calculateAnimatedAlpha( + lazyListState = lazyListState, + index = index, + ), + ), + contentAlignment = Alignment.Center, + ) { + itemContent(item) } - 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)) - }, - ) } } } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt index 24024c055e..fb8e139966 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt @@ -89,7 +89,9 @@ fun Modifier.showSoftKeyboard(show: Boolean): Modifier = if (show) { * For TextField, this modifier will clear focus when soft * keyboard is hidden. */ -fun Modifier.clearFocusOnSoftKeyboardHide(): Modifier = composed { +fun Modifier.clearFocusOnSoftKeyboardHide( + onFocusCleared: (() -> Unit)? = null, +): Modifier = composed { var isFocused by remember { mutableStateOf(false) } var keyboardShowedSinceFocused by remember { mutableStateOf(false) } if (isFocused) { @@ -100,6 +102,7 @@ fun Modifier.clearFocusOnSoftKeyboardHide(): Modifier = composed { keyboardShowedSinceFocused = true } else if (keyboardShowedSinceFocused) { focusManager.clearFocus() + onFocusCleared?.invoke() } } }