I am working in Android Jetpack Compose.
I have three TextField in a in a Column. I would like the third and/or last text field to be scrollable. How can I do that?
TextField example with horizontalScroll
#Composable
fun Test() {
TextField(
value = "long1 long2 long3 long4 long5 long6 long7 long8 long9 long10 long11 long12 text",
onValueChange = {},
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
)
}
TextField example with scrollable
#Composable
fun Test() {
var offset by remember { mutableStateOf(0f) }
TextField(value = "long1 long2 long3 long4 long5 long6 long7 long8 long9 long10 long11 long12 text",
onValueChange = {},
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.scrollable(
orientation = Orientation.Horizontal,
state = rememberScrollableState { delta ->
offset += delta
delta
}
)
)
}
more info there
https://developer.android.com/jetpack/compose/gestures?authuser=1#scrollable-modifier.
Related
I'm creating a custom slider control for my app but I can't avoid unnecessary recomposition without adding some ugly hacks...
CustomSlider1 is a component that recomposes all its children when the value changes; CustomSlider2 is what I came up with that does not... but the code doesn't seem right, so could anyone tell me what I'm doing wrong in CustomSlider1 and if CustomSlider2 is indeed correct?
The difference between the 2 components is basically that I read the value through a lambda and also added the Slider component inside a Scoped composable.
I'm using recomposeHighlighter to show recompositions.
Here's a gif showing how both behaves when I change its value:
Here's the code:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TestTheme {
Column {
var value by remember {
mutableStateOf(50f)
}
CustomSlider1("Custom Slider", value, 50f, true, { value = it }, 0f..100f, 5)
Spacer(modifier = Modifier.padding(10.dp))
CustomSlider2("Custom Slider 2", { value }, 50f, true, { value = it }, 0f..100f, 5)
}
}
}
}
}
#Composable
fun CustomSlider1(
label: String,
value: Float,
defaultValue: Float,
enabled: Boolean = true,
onValueChange: (Float) -> Unit,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int = 0,
) {
Column(
modifier = Modifier.recomposeHighlighter()
) {
Text(
text = label,
color = if (enabled) Color.Unspecified else LocalContentColor.current.copy(alpha = 0.5f),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.recomposeHighlighter()
)
Row {
Slider(
value = value,
valueRange = valueRange,
steps = steps,
enabled = enabled,
onValueChange = onValueChange,
modifier = Modifier
.recomposeHighlighter()
.weight(1f)
)
IconButton(
onClick = { onValueChange(defaultValue) },
enabled = enabled,
colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.primary),
modifier = Modifier.recomposeHighlighter()
) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = null,
modifier = Modifier.recomposeHighlighter()
)
}
}
}
}
#Composable
fun CustomSlider2(
label: String,
value: () -> Float,
defaultValue: Float,
enabled: Boolean = true,
onValueChange: (Float) -> Unit,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int = 0,
) {
Column(
modifier = Modifier.recomposeHighlighter()
) {
Text(
text = label,
color = if (enabled) Color.Unspecified else LocalContentColor.current.copy(alpha = 0.5f),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.recomposeHighlighter()
)
Row {
Scoped { //had to do this to avoid recompositions...
Slider(
value = value.invoke(),
valueRange = valueRange,
steps = steps,
enabled = enabled,
onValueChange = onValueChange,
modifier = Modifier
.recomposeHighlighter()
.weight(1f)
)
}
IconButton(
onClick = { onValueChange(defaultValue) },
enabled = enabled,
colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.primary),
modifier = Modifier.recomposeHighlighter()
) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = null,
modifier = Modifier.recomposeHighlighter()
)
}
}
}
}
#Composable
fun Scoped(content: #Composable () -> Unit) = content()
First thing you do to prevent recompositions creating Scope to create recomposition scope to limit recomposition since Column and Row are inline functions that do not create scopes.
Second thing with lambdas. In compose lambdas are unique they defer state read from composition phase of frame to layout or draw phases that's why you don't have recompositions.
Composition->Layout( measure and Layout)->Draw are the phases when a (re)composition is triggered by using lambdas you don't invoke composition phase.
For lambdas and state deferring you can check out official document or question below
// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(Modifier.fillMaxSize().background(color))
Here, the box's background color is switching rapidly between two
colors. This state is thus changing very frequently. The composable
then reads this state in the background modifier. As a result, the box
has to recompose on every frame, since the color is changing on every
frame.
To improve this, we can use a lambda-based modifier–in this case,
drawBehind. That means the color state is only read during the draw
phase. As a result, Compose can skip the composition and layout phases
entirely–when the color changes, Compose goes straight to the draw
phase.
val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier
.fillMaxSize()
.drawBehind {
drawRect(color)
} )
Android Jetpack Compose - Composable Function get recompose each time Text-field value changes
For scoped composition you can check out this question or other answer linked to it
I want to make the text to be resizing dynamically when I scroll and them to stay that size when I stop scrolling.
For example here the number 4 to be 20.sp and the numbers 3 and 5 to be 18.sp (one index away) and 2 - 6 to be 16.sp and so on. I think that could be created with
Modifier.scale(), but have not found a way for now.
#OptIn(ExperimentalSnapperApi::class)
#Composable
fun Example() {
val listStateHour = rememberLazyListState()
val hourTexts: List<String> = (1..23).map { it.toString().padStart(2, '0') }
Box(
modifier = Modifier.background(color = Color.LightGray).fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.height(32.dp)
.fillMaxWidth()
.background(color = Color.Black.copy(0.2f)),
) {}
LazyColumn(
modifier = Modifier.height(200.dp),
state = listStateHour,
horizontalAlignment = Alignment.Start,
contentPadding = PaddingValues(vertical = 116.dp),
flingBehavior = rememberSnapperFlingBehavior(lazyListState = listStateHour)
) {
itemsIndexed(hourTexts) { index, item ->
Box(
modifier = Modifier.height(32.dp),
contentAlignment = Alignment.Center
) {
Text(
text = item,
color = Color.Black,
fontSize = 20.sp
)
}
}
}
}
}
External dependencies that I am using -> implementation 'dev.chrisbanes.snapper:snapper:0.3.0'
You can observe listStateHour.firstVisibleItemIndex and calculate a scaling factor depending on the distance an item has to that value.
I didn't test it with the flingBehaviour you're using but this might help you:
val listStateHour = rememberLazyListState()
val firstVisibleItemIndex by remember { derivedStateOf { listStateHour.firstVisibleItemIndex } }
val hourTexts: List<String> = (1..23).map { it.toString().padStart(2, '0')
Box(
modifier = Modifier
.background(color = Color.LightGray)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.height(32.dp)
.fillMaxWidth()
.background(color = Color.Black.copy(0.2f)),
) {}
LazyColumn(
modifier = Modifier.height(200.dp),
state = listStateHour,
horizontalAlignment = Alignment.Start,
contentPadding = PaddingValues(vertical = 116.dp)
) {
itemsIndexed(hourTexts) { index, item ->
Box(
modifier = Modifier.height(32.dp),
contentAlignment = Alignment.Center
) {
Text(
text = item,
color = Color.Black,
fontSize = 20.sp,
modifier = Modifier.scale(
scaleText(
firstVisibleIndex = firstVisibleItemIndex,
itemIndex = index
)
)
)
}
}
}
}
fun scaleText(firstVisibleIndex: Int, itemIndex: Int): Float {
val distance = abs( firstVisibleIndex - itemIndex)
return 1f - distance * .2f
}
I have this outlined edit text, and I want to display a red error color when user tries to put '#' on the outline edit text when they're trying to sign in. I was wondering how I can handle error on an edit text in jetpack compose
UserInputTextField(
fieldState = usernameState.value,
onFieldChange = { usernameState.value = it },
label = "Enter Name",
)
#Composable
fun UserInputTextField(
fieldState: String,
onFieldChange: (String) -> Unit,
label: String,
modifier: Modifier = Modifier,
) {
androidx.compose.material.OutlinedTextField(
value = fieldState, onValueChange = {
onFieldChange(it)
},
label = { androidx.compose.material.Text(text = label) },
modifier = modifier
.fillMaxWidth()
.padding(top = 16.dp)
.semantics { testTag = TestTags.LoginContent.USERNAME_FIELD },
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Blue,
unfocusedBorderColor = Color.Black
)
)
}
Use isError attribute of TextField.
Example
isError = fieldState.contains("#"),
Sample code for reference
#Composable
fun ErrorCheck() {
val (text, setText) = remember {
mutableStateOf("")
}
UserInputTextField(
fieldState = text,
onFieldChange = setText,
label = "Email",
)
}
#Composable
fun UserInputTextField(
fieldState: String,
onFieldChange: (String) -> Unit,
label: String,
modifier: Modifier = Modifier,
) {
androidx.compose.material.OutlinedTextField(
value = fieldState,
onValueChange = {
onFieldChange(it)
},
isError = fieldState.contains("#"),
modifier = modifier
.fillMaxWidth()
.padding(
all = 16.dp,
),
colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Blue,
unfocusedBorderColor = Color.Black,
)
)
}
I want to change the text style (fontSize , color , fontWeight , ...) of a selected text in a TextFiled() composable , with a button in android jetpack compose.
(The main problem is, when i change the text style of a selected text ,the TextField can not save it , or when i add/remove a letter in TextField , the TextField deletes the previous text style.)
In other words, when the recomposition process occurs, the text styles disappears in the TextField()
my code is :
#Composable
fun Show() {
val inputText = remember{ mutableStateOf(TextFieldValue("This is a annotated text text"))}
Column(
modifier = Modifier.fillMaxSize().padding(5.dp) ,
horizontalAlignment = Alignment.CenterHorizontally
) {
//=================== TextField
CustomTextField(textInput = inputText)
//==================== Button
Button(onClick = {
inputText.value = changeSegmentColor(inputText.value)
}) {
Text(text = "Change the text style of selected text")
}
//======================
}
}
#Composable
fun CustomTextField (
textInput:MutableState<TextFieldValue>
) {
TextField(
value = textInput.value , onValueChange = {
textInput.value = it
},
modifier = Modifier.fillMaxWidth().heightIn(min = 200.dp) ,
)
}
private fun changeSegmentColor(textFVal: TextFieldValue):TextFieldValue{
val txtAnnotatedBuilder = AnnotatedString.Builder()
val realStartIndex = textFVal.getTextBeforeSelection(textFVal.text.length).length
val endIndex = realStartIndex + textFVal.getSelectedText().length
txtAnnotatedBuilder.append(textFVal.annotatedString)
val myStyle = SpanStyle(
color = Color.Red ,
fontSize = 16.sp ,
background = Color.Green
)
txtAnnotatedBuilder.addStyle(myStyle ,realStartIndex ,endIndex)
return textFVal.copy(annotatedString = txtAnnotatedBuilder.toAnnotatedString())
}
I am going to design a layout with a bottomsheetscaffold with sheetPeekHeight to be 100 dp in order to show the sheet content. I also need to put a lazyColumn for the main content of the bottomsheetscaffold. But when the lazy column scrolls to the end, the final item will be behind the bottom sheet. How can I make the final item of the column be above the sheet?
Here is the code for the bottom sheet:
#ExperimentalMaterialApi
#Composable
fun HomeScreen() {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
Text("this is test", modifier = Modifier.fillMaxWidth().height(60.dp))
},
sheetPeekHeight = 100.dp,
sheetShape = RoundedCornerShape(topEnd = 52.dp, topStart = 52.dp),
backgroundColor = Color.White
) {
MainContent()
}
}
#Composable
fun MainContent() {
LazyColumn {
items(count = 5) { itemIndex ->
when (itemIndex) {
0 -> {
Image(modifier = Modifier
.fillMaxWidth()
.height(100.dp), contentDescription = "test",
painter = painterResource(id = R.drawable.image))
}
}
}
}
}
Spacer(modifier=Modifier.height(100.dp)) I think fits better than a box here.
In your case it is easier to use fixed height, but if your content is dynamic you can also calculate bottomSheet height based on screenheight - bottomSheetOffset
fun YourComposable{
...
val bottomSheetHeight =
configuration.screenHeightDp.dp - bottomSheetScaffoldState.bottomSheetState.offset.value.pxToDp
...
}
private val Float.pxToDp: Dp
get() = (this / Resources.getSystem().displayMetrics.density).dp
As a workaround, I added an empty box with the same height as peekheight to the end of the lazycloumn. But I'm still interested in a better solution.