WheelPicker: Add manual input (#9338)

pull/9290/head
Ivan Iskandar 1 year ago committed by GitHub
parent bfb7b5afd5
commit 60d8650860
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

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

@ -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,81 +22,55 @@ 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,
items: List<Number>,
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(
WheelPicker(
modifier = modifier,
contentAlignment = Alignment.Center,
startIndex = startIndex,
items = items,
size = size,
onSelectionChanged = onSelectionChanged,
manualInputType = KeyboardType.Number,
backgroundContent = backgroundContent,
) {
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)
}
}
}
WheelPickerDefaults.Item(text = "$it")
}
}
@ -102,7 +78,7 @@ fun WheelPicker(
fun WheelTextPicker(
modifier: Modifier = Modifier,
startIndex: Int = 0,
texts: List<String>,
items: List<String>,
size: DpSize = DpSize(128.dp, 128.dp),
onSelectionChanged: (index: Int) -> Unit = {},
backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
@ -112,122 +88,126 @@ fun WheelTextPicker(
WheelPicker(
modifier = modifier,
startIndex = startIndex,
count = remember(texts) { texts.size },
items = items,
size = size,
onSelectionChanged = onSelectionChanged,
backgroundContent = backgroundContent,
) {
WheelPickerDefaults.Item(text = texts[it])
WheelPickerDefaults.Item(text = it)
}
}
@Composable
fun WheelDatePicker(
private fun <T> WheelPicker(
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<T>,
size: DpSize = DpSize(128.dp, 128.dp),
onSelectionChanged: (index: Int) -> Unit = {},
manualInputType: KeyboardType? = null,
backgroundContent: (@Composable (size: DpSize) -> Unit)? = {
WheelPickerDefaults.Background(size = it)
},
onSelectionChanged: (date: LocalDate) -> Unit = {},
itemContent: @Composable LazyItemScope.(item: T) -> Unit,
) {
var internalSelection by remember { mutableStateOf(startDate) }
val internalOnSelectionChange: (LocalDate) -> Unit = {
internalSelection = it
onSelectionChanged(internalSelection)
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 maxMonth = if (maxDate?.year == internalSelection.year) {
maxDate.monthValue
} else {
12
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)
}
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))
},
)
}
}
}

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

Loading…
Cancel
Save