mirror of
https://github.com/bitwarden/android.git
synced 2024-11-24 10:25:57 +03:00
Add reusable slider component (#3974)
This commit is contained in:
parent
f349a72f72
commit
80c1c26f5c
2 changed files with 167 additions and 134 deletions
|
@ -0,0 +1,160 @@
|
|||
package com.x8bit.bitwarden.ui.platform.components.slider
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toDp
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* A custom Bitwarden-themed slider.
|
||||
*
|
||||
* @param value The currently set value.
|
||||
* @param range The range of values allowed.
|
||||
* @param onValueChange Lambda callback for when the value changes and whether the change was from
|
||||
* user interaction or not.
|
||||
* @param modifier The [Modifier] to be applied to this radio button.
|
||||
* @param sliderTag The option test tag for the slider component.
|
||||
* @param valueTag The option test tag for the value field component.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun BitwardenSlider(
|
||||
value: Int,
|
||||
range: ClosedRange<Int>,
|
||||
onValueChange: (value: Int, isUserInteracting: Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sliderTag: String? = null,
|
||||
valueTag: String? = null,
|
||||
) {
|
||||
val sliderValue by rememberUpdatedState(newValue = value.coerceIn(range = range))
|
||||
var labelTextWidth by remember { mutableStateOf(value = Dp.Unspecified) }
|
||||
val density = LocalDensity.current
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.semantics(mergeDescendants = true) {},
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = sliderValue.toString(),
|
||||
readOnly = true,
|
||||
onValueChange = { },
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.length),
|
||||
modifier = Modifier.onGloballyPositioned { layoutCoordinates ->
|
||||
if (labelTextWidth == Dp.Unspecified) {
|
||||
labelTextWidth = layoutCoordinates.size.width.toDp(density = density)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier
|
||||
.onPreviewKeyEvent { keyEvent ->
|
||||
when (keyEvent.key) {
|
||||
Key.DirectionUp -> {
|
||||
onValueChange(sliderValue + 1, true)
|
||||
true
|
||||
}
|
||||
|
||||
Key.DirectionDown -> {
|
||||
onValueChange(sliderValue - 1, true)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
.semantics { valueTag?.let { testTag = it } }
|
||||
.wrapContentWidth()
|
||||
// We want the width to be no wider than the label + 16dp on either side
|
||||
.width(width = 16.dp + labelTextWidth + 16.dp),
|
||||
)
|
||||
|
||||
val colors = SliderDefaults.colors(
|
||||
activeTickColor = Color.Transparent,
|
||||
inactiveTickColor = Color.Transparent,
|
||||
disabledActiveTickColor = Color.Transparent,
|
||||
disabledInactiveTickColor = Color.Transparent,
|
||||
)
|
||||
Slider(
|
||||
value = sliderValue.toFloat(),
|
||||
onValueChange = { newValue -> onValueChange(newValue.toInt(), true) },
|
||||
onValueChangeFinished = { onValueChange(sliderValue, false) },
|
||||
valueRange = range.start.toFloat()..range.endInclusive.toFloat(),
|
||||
steps = range.endInclusive - 1,
|
||||
colors = colors,
|
||||
thumb = {
|
||||
SliderDefaults.Thumb(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
colors = colors,
|
||||
thumbSize = DpSize(width = 20.dp, height = 20.dp),
|
||||
)
|
||||
},
|
||||
track = { sliderState ->
|
||||
SliderDefaults.Track(
|
||||
modifier = Modifier.height(height = 4.dp),
|
||||
drawStopIndicator = null,
|
||||
colors = colors,
|
||||
sliderState = sliderState,
|
||||
thumbTrackGapSize = 0.dp,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.focusProperties { canFocus = false }
|
||||
.semantics { sliderTag?.let { testTag = it } }
|
||||
.weight(weight = 1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun BitwardenSlider_preview() {
|
||||
BitwardenTheme {
|
||||
BitwardenSlider(
|
||||
value = 6,
|
||||
range = 0..10,
|
||||
onValueChange = { _, _ -> },
|
||||
)
|
||||
}
|
||||
}
|
|
@ -2,28 +2,19 @@
|
|||
|
||||
package com.x8bit.bitwarden.ui.tools.feature.generator
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
|
@ -31,26 +22,13 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
@ -59,7 +37,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.LivecycleEventEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toDp
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem
|
||||
|
@ -73,6 +50,7 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextFieldWithAc
|
|||
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.slider.BitwardenSlider
|
||||
import com.x8bit.bitwarden.ui.platform.components.stepper.BitwardenStepper
|
||||
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenPolicyWarningText
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenWideSwitch
|
||||
|
@ -526,11 +504,12 @@ private fun ColumnScope.PasswordTypeContent(
|
|||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
PasswordLengthSliderItem(
|
||||
length = passwordTypeState.length,
|
||||
onPasswordSliderLengthChange = passwordHandlers.onPasswordSliderLengthChange,
|
||||
minValue = passwordTypeState.minLength,
|
||||
maxValue = passwordTypeState.maxLength,
|
||||
BitwardenSlider(
|
||||
value = passwordTypeState.length,
|
||||
onValueChange = passwordHandlers.onPasswordSliderLengthChange,
|
||||
range = passwordTypeState.minLength..passwordTypeState.maxLength,
|
||||
sliderTag = "PasswordLengthSlider",
|
||||
valueTag = "PasswordLengthLabel",
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
@ -595,112 +574,6 @@ private fun ColumnScope.PasswordTypeContent(
|
|||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun PasswordLengthSliderItem(
|
||||
length: Int,
|
||||
onPasswordSliderLengthChange: (value: Int, isUserInteracting: Boolean) -> Unit,
|
||||
minValue: Int,
|
||||
maxValue: Int,
|
||||
) {
|
||||
val sliderValue by rememberUpdatedState(newValue = length.coerceIn(minValue, maxValue))
|
||||
var labelTextWidth by remember { mutableStateOf(Dp.Unspecified) }
|
||||
|
||||
val density = LocalDensity.current
|
||||
val sliderRange = minValue.toFloat()..maxValue.toFloat()
|
||||
|
||||
val lengthLabel: @Composable () -> Unit = remember {
|
||||
{
|
||||
Text(
|
||||
text = stringResource(id = R.string.length),
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned { layoutCoordinates ->
|
||||
if (labelTextWidth == Dp.Unspecified) {
|
||||
labelTextWidth = layoutCoordinates.size.width.toDp(density)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.semantics(mergeDescendants = true) {},
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = sliderValue.toString(),
|
||||
readOnly = true,
|
||||
onValueChange = { },
|
||||
label = lengthLabel,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier
|
||||
.onPreviewKeyEvent { keyEvent ->
|
||||
when (keyEvent.key) {
|
||||
Key.DirectionUp -> {
|
||||
onPasswordSliderLengthChange(sliderValue + 1, true)
|
||||
true
|
||||
}
|
||||
|
||||
Key.DirectionDown -> {
|
||||
onPasswordSliderLengthChange(sliderValue - 1, true)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
.testTag("PasswordLengthLabel")
|
||||
.wrapContentWidth()
|
||||
// We want the width to be no wider than the label + 16dp on either side
|
||||
.width(16.dp + labelTextWidth + 16.dp),
|
||||
)
|
||||
|
||||
val colors = SliderDefaults.colors(
|
||||
activeTickColor = Color.Transparent,
|
||||
inactiveTickColor = Color.Transparent,
|
||||
disabledActiveTickColor = Color.Transparent,
|
||||
disabledInactiveTickColor = Color.Transparent,
|
||||
)
|
||||
Slider(
|
||||
value = sliderValue.toFloat(),
|
||||
onValueChange = { newValue ->
|
||||
onPasswordSliderLengthChange(newValue.toInt(), true)
|
||||
},
|
||||
onValueChangeFinished = {
|
||||
onPasswordSliderLengthChange(sliderValue, false)
|
||||
},
|
||||
valueRange = sliderRange,
|
||||
steps = maxValue - 1,
|
||||
colors = colors,
|
||||
thumb = {
|
||||
SliderDefaults.Thumb(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
colors = colors,
|
||||
thumbSize = DpSize(width = 20.dp, height = 20.dp),
|
||||
)
|
||||
},
|
||||
track = { sliderState ->
|
||||
SliderDefaults.Track(
|
||||
modifier = Modifier.height(height = 4.dp),
|
||||
drawStopIndicator = null,
|
||||
colors = colors,
|
||||
sliderState = sliderState,
|
||||
thumbTrackGapSize = 0.dp,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.focusProperties { canFocus = false }
|
||||
.testTag(tag = "PasswordLengthSlider")
|
||||
.weight(weight = 1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasswordCapitalLettersToggleItem(
|
||||
useCapitals: Boolean,
|
||||
|
|
Loading…
Reference in a new issue