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,
properties = DialogProperties(
usePlatformDefaultWidth = false,
decorFitsSystemWindows = false,
),
) {
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_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))
}
)
}
}
}

View file

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

View file

@ -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<String>,
items: List<Number>,
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<String>,
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 <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 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))
},
)
}
}
}

View file

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