If we want our TextField to have an equivalent behaviour to EditText:selectAllOnFocus = "true" we can do something like create a TextFieldValue and set the selection from zero to lenght like TextRange(0, text.length)
This works and when user focus the TextField the whole text get selected, the problem is that when we create a TextFieldValue we need to set selection, the default value is Zero.
If the user wants to drag the cursor he just cant. Is there a way for now to create a selectAllOnFocus behaviour that allows the user to drag the cursor all over the text if he wants to in compose?
Yes I think one way is to follow the result selection coming from the TextField onValueChange callback:
var editableText by rememberSaveable { mutableStateOf("some text") }
val textRangeStart = rememberSaveable { mutableStateOf(0) }
val textRangeEnd = rememberSaveable { mutableStateOf(0) }
val textField = remember(editableText, textRangeEnd.value, textRangeStart.value) {
mutableStateOf(
TextFieldValue(
text = editableText ?: "",
selection = TextRange(textRangeStart.value, textRangeEnd.value)
)
)
}
TextField(
modifier = Modifier.fillMaxSize(),
value = textField.value,
placeholder = { Text("") },
onValueChange = {
textEditorViewModel.editableText.value = it.text
textRangeStart.value = it.selection.start
textRangeEnd.value = it.selection.end
}
)
}
Related
#Composable
fun SliderWithCustomTrackAndThumb() {
var sliderPosition by remember { mutableStateOf(0f) }
val interactionSource = MutableInteractionSource()
val colors = SliderDefaults.colors(thumbColor = Color.Red, activeTrackColor = Color.Red)
Column {
Text(text = sliderPosition.toString())
Slider(
modifier = Modifier.semantics { contentDescription = "Localized Description" },
value = sliderPosition,
onValueChange = { sliderPosition = it },
valueRange = 0f..100f,
onValueChangeFinished = {
// launch some business logic update with the state you hold
// viewModel.updateSelectedSliderValue(sliderPosition)
},
interactionSource = interactionSource,
thumb = {
SliderDefaults.Thumb(
interactionSource = interactionSource,
colors = colors
)
},
track = { sliderPositions ->
SliderDefaults.Track(
colors = colors,
sliderPositions = sliderPositions
)
}
)
}
}
The above composable function creates a sample slider with a custom thumb size. I want to increase the size of the track. how can I do that?
while you can't change the size size of the track set in the SlideDefaults.Track()component, you can always provide your own track implementation by copying the code from the SliderDefaults and change the track size there.
I have a composable with with a remembered value called imageWidth
var imageWidth = remember { 0f }
I want to calculated the image width once (unless rotation has changed). This is the only place that writes to that variable.
LaunchedEffect(key1 = currentRotation) {
val ratio = imageBitmap.getRatio(currentRotation)
imageWidth = //some calculation
}
The imageWidth is accessed by multiple places in the app. Using some log prints, I have made sure that this side effect is being called and correct value is set to imageWidth for the first time I enter the screen.
For some reason, by clicking on some button, the value is being reset to 0.
What are the cases that a remembered value can be forgotten/reset?
If the reason is the disposal of the composable, why isn't the LaunchedEffect called again and calculate the value when entering the composition?
With your current set up what supposed to happen is imageWidth to be set to 0f after each recomposition because remember is run on composition or any of its key are changed.
/**
* Remember the value returned by [calculation] if all values of [keys] are equal to the previous
* composition, otherwise produce and remember a new value by calling [calculation].
*/
#Composable
inline fun <T> remember(
vararg keys: Any?,
calculation: #DisallowComposableCalls () -> T
): T {
var invalid = false
for (key in keys) invalid = invalid or currentComposer.changed(key)
return currentComposer.cache(invalid, calculation)
}
In LaunchedEffect you set value but when another recomposition happens it's reset to value in remember block.
However something i might have missed here is by rotation if you mean rotating device and recreating Activity, the answer below doesn't work, you can move your value to ViewModel to store latest value or rememberSavable.
If your Activity is not recreated but you want to recalculate block inside remember add keys to check if there has to be new calculation.
You need to add currentRotation as key for it to be set only when rotation changes
var imageWidth = remember(currentRotation) {
val ratio = imageBitmap.getRatio(currentRotation)
//some calculation result as float
}
remember with keys is commonly used in default Composable source codes, but i wonder why it' not mentioned in official documents.
Slider for instance use it as
#Composable
fun Slider(
value: Float,
onValueChange: (Float) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
/*#IntRange(from = 0)*/
steps: Int = 0,
onValueChangeFinished: (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: SliderColors = SliderDefaults.colors()
) {
require(steps >= 0) { "steps should be >= 0" }
val onValueChangeState = rememberUpdatedState(onValueChange)
val tickFractions = remember(steps) {
stepsToTickFractions(steps)
}
// rest of the code
}
And in painterResource code
#Composable
#ComposableOpenTarget(-1)
fun rememberVectorPainter(
defaultWidth: Dp,
defaultHeight: Dp,
viewportWidth: Float = Float.NaN,
viewportHeight: Float = Float.NaN,
name: String = RootGroupName,
tintColor: Color = Color.Unspecified,
tintBlendMode: BlendMode = BlendMode.SrcIn,
autoMirror: Boolean = false,
content: #Composable #VectorComposable (viewportWidth: Float, viewportHeight: Float) -> Unit
): VectorPainter {
val density = LocalDensity.current
val widthPx = with(density) { defaultWidth.toPx() }
val heightPx = with(density) { defaultHeight.toPx() }
val vpWidth = if (viewportWidth.isNaN()) widthPx else viewportWidth
val vpHeight = if (viewportHeight.isNaN()) heightPx else viewportHeight
val intrinsicColorFilter = remember(tintColor, tintBlendMode) {
if (tintColor != Color.Unspecified) {
ColorFilter.tint(tintColor, tintBlendMode)
} else {
null
}
}
return remember { VectorPainter() }.apply {
// These assignments are thread safe as parameters are backed by a mutableState object
size = Size(widthPx, heightPx)
this.autoMirror = autoMirror
this.intrinsicColorFilter = intrinsicColorFilter
RenderVector(name, vpWidth, vpHeight, content)
}
}
This is a very common approach in many default Composables and the most common one is LaunchedEffect
#Composable
#NonRestartableComposable
#OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block) }
}
which runs code block on first composition or when at least one of they keys change.
When I am trying to put some composable after RangeSlider, RangeSlider fills all width.
For example:
Row {
Text(text = "Test1")
var range by remember { mutableStateOf(-20f..20f) }
RangeSlider(
values = range, onValueChange = {
range = it
},
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colorScheme.onSecondaryContainer,
activeTrackColor = MaterialTheme.colorScheme.onSecondaryContainer
),
valueRange = -50f..50f
)
Text(text = "Test2")
}
In this case Text with Test2 is invisible.
When I am trying to force RangeSlider to be with some width, second slider is out of track. Also, Modifier.weigth() does not work.
Using another Layout that wraps RangeSlider solves the issue. Don't know why RangeSlider does not abide Modifier.weigth()
Row {
Text(text = "Test1")
var range by remember { mutableStateOf(-20f..20f) }
Row(
modifier= Modifier.weight(1f),
){
RangeSlider(
values = range, onValueChange = {
range = it
},
valueRange = -50f..50f
)
}
Text(text = "Test2")
}
When i click "Next" ImeAction, the cursor position goes to a previous remembered position (i.e. if you ever manually set a cursor position) for newly selected TextField. If there was none, then the cursor jumps to the start, even though the TextField is not empty. I expected the cursor always be in the end, when focus was gained via ImeAction.
Sample code (for simplicity i hold mutable states right here, not using ViewModel):
#Composable
fun ImeActionIsBroken() {
val focusRequester = remember { FocusRequester() }
Column {
var value1 by rememberSaveable { mutableStateOf("Some value 1") }
TextField(
value = value1,
onValueChange = { value1 = it },
modifier = Modifier.fillMaxWidth(),
keyboardActions = KeyboardActions {
focusRequester.requestFocus()
},
singleLine = true
)
var value2 by rememberSaveable { mutableStateOf("Some value 2") }
TextField(
value = value2,
onValueChange = { value2 = it },
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
singleLine = true
)
}
}
Compose version 1.0.5
You can use the TextFieldValue object instead of using String for our TextField. With that, you can customize the selected range of text / or where the text cursor should be.
In your example, you should move up the declaration of the value2 to be accessible by the KeyboardAction callback of the first text field.
Then update it to be a TextFieldValue, like so :
var value2 by rememberSaveable { mutableStateOf(TextFieldValue("Some value 2")) }
And inside the KeyboardAction you can just update the selection to be at the end of the field, before requesting the focus, like that :
value2 = value2.copy(selection = TextRange(value2.text.length))
focusRequester.requestFocus()
I'm trying to implement a date picker view on my app however, when I select a date on the calendar view it updates the text to one day prior instead of the selected date. I'm assuming it has to do with the formatting function. I'm using Android Jetpack Compose and followed a tutorial from Medium that explains the implementation but its not working as expected. Thank you in advance.
#ExperimentalComposeApi
#Composable
fun DatePickerview() {
var datePicked: String? by remember {
mutableStateOf(null)
}
val updatedDate = { date: Long? ->
datePicked = DateFormater(date)
}
val activity = LocalContext.current as AppCompatActivity
Box(
modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.TopStart)
.padding(16.dp)
.border(3.dp, DarkerButtonBlue, RoundedCornerShape(10.dp))
.clickable {
showDatePicker(activity, updatedDate)
}
) {
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
val (label, iconView) = createRefs()
Text(
text = datePicked ?: "Event Date",
color = Color.White,
fontSize = 20.sp,
modifier = Modifier
.fillMaxWidth()
.constrainAs(label) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(iconView.start)
width = Dimension.fillToConstraints
}
)
Icon(
imageVector = Icons.Default.DateRange,
contentDescription = null,
modifier = Modifier
.size(30.dp)
.constrainAs(iconView) {
end.linkTo(parent.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
},
tint = Color.DarkGray
)
}
}
}
fun DateFormater(milliseconds: Long?): String? {
milliseconds?.let {
val formatter = SimpleDateFormat("MM/dd/yyyy", Locale.US)
val calendar: Calendar = Calendar.getInstance()
calendar.setTimeInMillis(it)
return formatter.format(calendar.getTime())
}
return null
}
fun showDatePicker(
activity: AppCompatActivity,
updateDate: (Long?) -> Unit
) {
val picker = MaterialDatePicker.Builder.datePicker().build()
picker.show(activity.supportFragmentManager, picker.toString())
picker.addOnPositiveButtonClickListener {
updateDate(it)
}
}
Most likely your problem is with your time zone, try specifying it:
fun DateFormater(milliseconds: Long?): String? {
milliseconds?.let {
val formatter = java.text.SimpleDateFormat("MM/dd/yyyy", Locale.US)
val calendar: Calendar = Calendar.getInstance()
formatter.timeZone = calendar.timeZone
calendar.timeInMillis = it
return formatter.format(calendar.time)
}
return null
}
Also a little off-topic. Using ConstraintLayout for such a simple layout is unnecessary. You can use Row with the weight modifier for your text: in this case the size of Icon will be calculated before the size of Text, see weight modifier documentation for details.
Row(
modifier = Modifier
.padding(16.dp)
) {
Text(
text = datePicked ?: "Event Date",
color = Color.White,
fontSize = 20.sp,
modifier = Modifier
.weight(1f)
)
Icon(
imageVector = Icons.Default.DateRange,
contentDescription = null,
tint = Color.DarkGray,
)
}