WheelPicker: Add manual input (#9338)

This commit is contained in:
Ivan Iskandar 2023-04-15 20:26:33 +07:00 committed by GitHub
parent bfb7b5afd5
commit 60d8650860
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 159 additions and 177 deletions

View File

@ -84,6 +84,7 @@ fun AdaptiveSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
properties = DialogProperties( properties = DialogProperties(
usePlatformDefaultWidth = false, usePlatformDefaultWidth = false,
decorFitsSystemWindows = false,
), ),
) { ) {
AdaptiveSheetImpl( AdaptiveSheetImpl(

View File

@ -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_HAS_UNREAD
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ 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.WheelPickerDefaults
import tachiyomi.presentation.core.components.WheelTextPicker
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -334,28 +334,25 @@ object SettingsLibraryScreen : SearchableSettings {
modifier = modifier, modifier = modifier,
contentAlignment = Alignment.Center, 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) val size = DpSize(width = maxWidth / 2, height = 128.dp)
Row { Row {
WheelPicker( val columns = (0..10).map { getColumnValue(value = it) }
size = size, WheelTextPicker(
count = 11,
startIndex = portraitValue, startIndex = portraitValue,
items = columns,
size = size,
onSelectionChanged = onPortraitChange, onSelectionChanged = onPortraitChange,
backgroundContent = null, backgroundContent = null,
) { index -> )
WheelPickerDefaults.Item(text = getColumnValue(value = index)) WheelTextPicker(
}
WheelPicker(
size = size,
count = 11,
startIndex = landscapeValue, startIndex = landscapeValue,
items = columns,
size = size,
onSelectionChanged = onLandscapeChange, onSelectionChanged = onLandscapeChange,
backgroundContent = null, backgroundContent = null,
) { index -> )
WheelPickerDefaults.Item(text = getColumnValue(value = index))
}
} }
} }
} }

View File

@ -30,6 +30,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.components.ScrollbarLazyColumn
import tachiyomi.presentation.core.components.WheelNumberPicker
import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.components.material.AlertDialogContent import tachiyomi.presentation.core.components.material.AlertDialogContent
import tachiyomi.presentation.core.components.material.Divider import tachiyomi.presentation.core.components.material.Divider
@ -96,10 +97,10 @@ fun TrackChapterSelector(
BaseSelector( BaseSelector(
title = stringResource(R.string.chapters), title = stringResource(R.string.chapters),
content = { content = {
WheelTextPicker( WheelNumberPicker(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
startIndex = selection, startIndex = selection,
texts = range.map { "$it" }, items = range.toList(),
onSelectionChanged = { onSelectionChange(it) }, onSelectionChanged = { onSelectionChange(it) },
) )
}, },
@ -122,7 +123,7 @@ fun TrackScoreSelector(
WheelTextPicker( WheelTextPicker(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
startIndex = selections.indexOf(selection).coerceAtLeast(0), startIndex = selections.indexOf(selection).coerceAtLeast(0),
texts = selections, items = selections,
onSelectionChanged = { onSelectionChange(selections[it]) }, onSelectionChanged = { onSelectionChange(selections[it]) },
) )
}, },

View File

@ -4,15 +4,17 @@ import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListItemInfo import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape 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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -20,38 +22,102 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback 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.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import java.text.DateFormatSymbols import tachiyomi.presentation.core.util.clearFocusOnSoftKeyboardHide
import java.time.LocalDate import tachiyomi.presentation.core.util.clickableNoIndication
import tachiyomi.presentation.core.util.showSoftKeyboard
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@Composable @Composable
fun WheelPicker( fun WheelNumberPicker(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
startIndex: Int = 0, startIndex: Int = 0,
count: Int, items: List<Number>,
size: DpSize = DpSize(128.dp, 128.dp), size: DpSize = DpSize(128.dp, 128.dp),
onSelectionChanged: (index: Int) -> Unit = {}, onSelectionChanged: (index: Int) -> Unit = {},
backgroundContent: (@Composable (size: DpSize) -> Unit)? = { backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
WheelPickerDefaults.Background(size = it) WheelPickerDefaults.Background(size = it)
}, },
itemContent: @Composable LazyItemScope.(index: Int) -> Unit,
) { ) {
val lazyListState = rememberLazyListState(startIndex) WheelPicker(
modifier = modifier,
startIndex = startIndex,
items = items,
size = size,
onSelectionChanged = onSelectionChanged,
manualInputType = KeyboardType.Number,
backgroundContent = backgroundContent,
) {
WheelPickerDefaults.Item(text = "$it")
}
}
@Composable
fun WheelTextPicker(
modifier: Modifier = Modifier,
startIndex: Int = 0,
items: 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,
items = items,
size = size,
onSelectionChanged = onSelectionChanged,
backgroundContent = backgroundContent,
) {
WheelPickerDefaults.Item(text = it)
}
}
@Composable
private fun <T> WheelPicker(
modifier: Modifier = Modifier,
startIndex: Int = 0,
items: List<T>,
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 haptic = LocalHapticFeedback.current
val lazyListState = rememberLazyListState(startIndex)
var internalIndex by remember { mutableStateOf(startIndex) }
val internalOnSelectionChanged: (Int) -> Unit = {
internalIndex = it
onSelectionChanged(it)
}
LaunchedEffect(lazyListState, onSelectionChanged) { LaunchedEffect(lazyListState, onSelectionChanged) {
snapshotFlow { lazyListState.firstVisibleItemScrollOffset } snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
@ -60,25 +126,72 @@ fun WheelPicker(
.drop(1) .drop(1)
.collectLatest { .collectLatest {
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
onSelectionChanged(it) internalOnSelectionChanged(it)
} }
} }
Box( Box(
modifier = modifier, modifier = modifier
.height(size.height)
.width(size.width),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
backgroundContent?.invoke(size) backgroundContent?.invoke(size)
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)))
}
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( LazyColumn(
modifier = Modifier modifier = Modifier
.height(size.height) .let {
.width(size.width), if (manualInputType != null) {
it.clickableNoIndication { showManualInput = true }
} else {
it
}
},
state = lazyListState, state = lazyListState,
contentPadding = PaddingValues(vertical = size.height / RowCount * ((RowCount - 1) / 2)), contentPadding = PaddingValues(vertical = size.height / RowCount * ((RowCount - 1) / 2)),
flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState), flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState),
) { ) {
items(count) { index -> itemsIndexed(items) { index, item ->
Box( Box(
modifier = Modifier modifier = Modifier
.height(size.height / RowCount) .height(size.height / RowCount)
@ -91,145 +204,12 @@ fun WheelPicker(
), ),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
itemContent(index) itemContent(item)
} }
} }
} }
} }
} }
@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 { private fun LazyListState.snapOffsetForItem(itemInfo: LazyListItemInfo): Int {

View File

@ -89,7 +89,9 @@ fun Modifier.showSoftKeyboard(show: Boolean): Modifier = if (show) {
* For TextField, this modifier will clear focus when soft * For TextField, this modifier will clear focus when soft
* keyboard is hidden. * keyboard is hidden.
*/ */
fun Modifier.clearFocusOnSoftKeyboardHide(): Modifier = composed { fun Modifier.clearFocusOnSoftKeyboardHide(
onFocusCleared: (() -> Unit)? = null,
): Modifier = composed {
var isFocused by remember { mutableStateOf(false) } var isFocused by remember { mutableStateOf(false) }
var keyboardShowedSinceFocused by remember { mutableStateOf(false) } var keyboardShowedSinceFocused by remember { mutableStateOf(false) }
if (isFocused) { if (isFocused) {
@ -100,6 +102,7 @@ fun Modifier.clearFocusOnSoftKeyboardHide(): Modifier = composed {
keyboardShowedSinceFocused = true keyboardShowedSinceFocused = true
} else if (keyboardShowedSinceFocused) { } else if (keyboardShowedSinceFocused) {
focusManager.clearFocus() focusManager.clearFocus()
onFocusCleared?.invoke()
} }
} }
} }