Does Jetpack Compose offer a Material AutoComplete TextView replacement? - android

In the process of migrating my app to Jetpack compose, I've come to a part of my app where a TextField needs autocompletion functionality.
However, as of version 1.0.0-alpha05, I couldn't find any functionality to achieve this using the Compose API. The closest thing I've found is the DropdownMenu and DropdownMenuItem composeables, but it seems like it would be a lot of manual plumbing required to create an autocomplete menu out of these.
The obvious thing to do is just wait for future updates to Jetpack Compose, of course. But I'm wondering, has anyone who encountered a this issue in their migrations found a solution?

No at least till v1.0.2
so I implemented a nice working one in compose available in this gist
I also put it here:
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.window.PopupProperties
#Composable
fun TextFieldWithDropdown(
modifier: Modifier = Modifier,
value: TextFieldValue,
setValue: (TextFieldValue) -> Unit,
onDismissRequest: () -> Unit,
dropDownExpanded: Boolean,
list: List<String>,
label: String = ""
) {
Box(modifier) {
TextField(
modifier = Modifier
.fillMaxWidth()
.onFocusChanged { focusState ->
if (!focusState.isFocused)
onDismissRequest()
},
value = value,
onValueChange = setValue,
label = { Text(label) },
colors = TextFieldDefaults.outlinedTextFieldColors()
)
DropdownMenu(
expanded = dropDownExpanded,
properties = PopupProperties(
focusable = false,
dismissOnBackPress = true,
dismissOnClickOutside = true
),
onDismissRequest = onDismissRequest
) {
list.forEach { text ->
DropdownMenuItem(onClick = {
setValue(
TextFieldValue(
text,
TextRange(text.length)
)
)
}) {
Text(text = text)
}
}
}
}
}
How to use it
val all = listOf("aaa", "baa", "aab", "abb", "bab")
val dropDownOptions = mutableStateOf(listOf<String>())
val textFieldValue = mutableStateOf(TextFieldValue())
val dropDownExpanded = mutableStateOf(false)
fun onDropdownDismissRequest() {
dropDownExpanded.value = false
}
fun onValueChanged(value: TextFieldValue) {
dropDownExpanded.value = true
textFieldValue.value = value
dropDownOptions.value = all.filter { it.startsWith(value.text) && it != value.text }.take(3)
}
#Composable
fun TextFieldWithDropdownUsage() {
TextFieldWithDropdown(
modifier = Modifier.fillMaxWidth(),
value = textFieldValue.value,
setValue = ::onValueChanged,
onDismissRequest = ::onDropdownDismissRequest,
dropDownExpanded = dropDownExpanded.value,
list = dropDownOptions.value,
label = "Label"
)

As of compose 1.1.0-alpha06, Compose Material now offers an ExposedDropdownMenu composable, API here, which can be used to implement a dropdown menu which facilitates the autocompletion process. The actual autocompletion logic has to be implemented yourself.
The API docs give the following usage example, for an editable field:
val options = listOf("Option 1", "Option 2", "Option 3", "Option 4", "Option 5")
var exp by remember { mutableStateOf(false) }
var selectedOption by remember { mutableStateOf("") }
ExposedDropdownMenuBox(expanded = exp, onExpandedChange = { exp = !exp }) {
TextField(
value = selectedOptionText,
onValueChange = { selectedOptionText = it },
label = { Text("Label") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = exp)
},
colors = ExposedDropdownMenuDefaults.textFieldColors()
)
// filter options based on text field value (i.e. crude autocomplete)
val filterOpts = options.filter { it.contains(selectedOption, ignoreCase = true) }
if (filterOpts.isNotEmpty()) {
ExposedDropdownMenu(expanded = exp, onDismissRequest = { exp = false }) {
filterOpts.forEach { option ->
DropdownMenuItem(
onClick = {
selectedOption = option
exp = false
}
) {
Text(text = option)
}
}
}
}
}

Checkout this code that I made using XML and using that layout inside compose
using AndroidView. We can use this solution until it is included by default in compose.
You can customize it and style it as you want. I have personally tried it in my project and it works fine
<!-- text_input_field.xml -->
<!-- You can style your textfield here in XML with styles -->
<!-- this file should be in res/layout -->
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textfield.TextInputLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<AutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Label"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
// TextFieldWithDropDown.kt
// TextField with dropdown was not included by default in jetpack compose (1.0.2) and less
#Composable
fun TextFieldWithDropDown(
items: List<String>,
selectedValue: String?,
modifier: Modifier = Modifier,
onSelect: (Int) -> Unit
) {
AndroidView(
factory = { context ->
val textInputLayout = TextInputLayout
.inflate(context, R.layout.text_input_field, null) as TextInputLayout
// If you need to use different styled layout for light and dark themes
// you can create two different xml layouts one for light and another one for dark
// and inflate the one you need here.
val autoCompleteTextView = textInputLayout.editText as? AutoCompleteTextView
val adapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, items)
autoCompleteTextView?.setAdapter(adapter)
autoCompleteTextView?.setText(selectedValue, false)
autoCompleteTextView?.setOnItemClickListener { _, _, index, _ -> onSelect(index) }
textInputLayout
},
update = { textInputLayout ->
// This block will be called when recomposition happens
val autoCompleteTextView = textInputLayout.editText as? AutoCompleteTextView
val adapter = ArrayAdapter(textInputLayout.context, android.R.layout.simple_list_item_1, items)
autoCompleteTextView?.setAdapter(adapter)
autoCompleteTextView?.setText(selectedValue, false)
},
modifier = modifier
)
}
// MainActivity.kt
// It's important to use AppCompatActivity instead of ComponentActivity to get the material
// look on our XML based textfield
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Column {
TextFieldWithDropDown(
items = listOf("One", "Two", "Three"),
selectedValue = "Two",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// You can also set the value to a state
index -> println("$index was selected")
}
}
}
}
}

As you said, there is no such component yet. You have two options: create your own custom using DropDownMenu and BaseTextField or using hybrid xml-autocomplete and compose screen through androidx.compose.ui.platform.ComposeView

Related

Jetpack Compose XML Interoperability with Accessibility headings

I have an Android XML layout as follows:
...
<View
android:layout_width="match_parent"
android:layout_height="#dimen/spacer"/>
<androidx.compose.ui.platform.ComposeView
android:id="#+id/input_compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
...
And in the XML portion I have components that are listed as Accessibility Headings, i.e. android:accessibilityHeading="true"
And my composable is defined as follows:
#Composable
fun JetpackComposeContainer(
headingInfo: String,
descriptionInfo: String,
labelInfo: String,
placeholderInfo: String
) {
Column {
Text(
text = headingInfo,
style = MaterialTheme.typography.h1,
modifier = Modifier.semantics { heading() }
)
Text(
text = descriptionInfo,
style = MaterialTheme.typography.caption,
)
JetpackComposeInput(labelInfo, placeholderInfo)
}
}
#Composable
fun JetpackComposeInput(labelInfo: String, placeholderInfo: String) {
var text by rememberSaveable { mutableStateOf("") }
TextField(
modifier = Modifier.fillMaxWidth(),
value = text,
label = { Text(labelInfo) },
placeholder = { Text(placeholderInfo) },
onValueChange = {
text = it
}
)
}
However when I navigate by headings in my app, not only is the heading ignored, the entire Jetpack Compose layout is grouped and I can "internally" focus on the TextField:
The announcement (as a toast) shows no heading, and it cannot be navigated to as a heading using TalkBack.
NOTE: When I have an entire Jetpack Compose view made up of several instances of these composables in a column, the view behaves as expected.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
// Behaves as expected
Column {
JetpackComposeContainer("One","First","Input 1","Test 1")
JetpackComposeContainer("Two","Second","Input 2","Test 2")
JetpackComposeContainer("Three","Third","Input 3","Test 3")
JetpackComposeContainer("Four","Fourth","Input 4","Test 4")
}
}
}
}
}

Jetpack Compose LargeTopAppBar Displays Title Twice

I'm trying to build a Jetpack Compose app with Scaffold and a LargeTopAppBar. I currently have a very simple UI with only the LargeTopAppBar in a Scaffold, but when I run my app I see two small titles at the top of the screen.
Any ideas why this is happening or how to fix it? My activity code is as follows
#OptIn(ExperimentalMaterial3Api::class)
class MainActivity : MonetCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launchWhenCreated {
monet.awaitMonetReady()
setContent {
TVTimeTheme(monetCompat = monet) {
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val topAppBarScrollState = rememberTopAppBarScrollState()
val scrollBehavior = remember(decayAnimationSpec) {
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
decayAnimationSpec, topAppBarScrollState
)
}
Scaffold (
topBar = {
LargeTopAppBar(
title = { Text(text = "movies") },
scrollBehavior = scrollBehavior
)
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding))
}
}
}
}
}
}
These helped me:
Use only material3 components (androidx.compose.material3.*) in TabBar, not material (androidx.compose.material.*) components
Remove defaultTextColor of titleLarge and bodyLarge in your typography
I ran across this issue when I set the color within the Text composable, instead of the titleContentColor associated with the TopAppBar composable.
I was doing this:
MediumTopAppBar(
title = {
Text(
text = "Example",
color = MaterialTheme.colorScheme.secondary
)
},
...
)
Instead, use the titleContentColor field on the MediumTopAppBar composable itself. That looks like this:
MediumTopAppBar(
title = {
Text(
text = "Example",
)
},
colors = TopAppBarDefaults.largeTopAppBarColors(
titleContentColor = MaterialTheme.colorScheme.secondary
)
)
Easy mistake to make but stumped me for a good 10 minutes or so!
I think it's because you have not put your navigation icon yet. Try this:
Scaffold(
topBar = {
LargeTopAppBar(
title = { Text("movies") },
navigationIcon = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = "menu icon"
)
}
},
actions = {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Filled.Favorite,
contentDescription = "favorite icon"
)
}
},
)
},
content = {innerPadding ->
Box(modifier = Modifier.padding(innerPadding))
}
)
Source
Just make sure that whether you use import androidx.compose.material3.Text instead of import androidx.compose.material.Text

Avoid accidental tap/touch in a Jetpack Compose Slider which is placed inside in a scrollable column

I have a Slider which is placed inside a Column which is scrollable. When i scroll through the components sometimes accidentally slider value changes because of accidental touches. How can i avoid this?
Should i be disable taps on slider? If yes how can i do it?
Is there any alternate like Nested scroll instead of Column which can prevent this from happening?
#Composable
fun ColumnScope.FilterRange(
title: String,
range: ClosedFloatingPointRange<Float>,
rangeText: String,
valueRange: ClosedFloatingPointRange<Float>,
onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
) {
Spacer(modifier = Modifier.height(Size_Regular))
Text(
text = title,
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.height(Size_X_Small))
Text(
text = rangeText,
style = MaterialTheme.typography.subtitle1
)
RangeSlider(
modifier = Modifier.fillMaxWidth(),
values = range,
valueRange = valueRange,
onValueChange = {
onValueChange(it)
})
Spacer(modifier = Modifier.height(Size_Small))
Divider(thickness = DividerSize)
}
I would disable the RangeSlider and only enable it when you tap on it. You disable it by tapping anywhere else within the Column. This is a similar behavior used to mimic losing focus. Here's an example:
class MainActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
var rangeEndabled by remember { mutableStateOf(false)}.apply { this.value }
var sliderPosition by remember { mutableStateOf(0f..100f) }
Text(text = sliderPosition.toString())
Column(modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.pointerInput(Unit) {
detectTapGestures(
onTap = {
rangeEndabled = false
}
)
}) {
repeat(30) {
Text(it.toString())
}
RangeSlider(
enabled = rangeEndabled,
values = sliderPosition,
onValueChange = { sliderPosition = it },
valueRange = 0f..100f,
onValueChangeFinished = {
// launch some business logic update with the state you hold
// viewModel.updateSelectedSliderValue(sliderPosition)
},
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onTap = {
rangeEndabled = true
}
)
}
)
repeat(30) {
Text(it.toString())
}
}
}
}
}

Jetpack compose display html in text

I have a string that contains html, how can I display this in a Jetpack compose Text?
In a TextView I would use a Spanned and do something like:
TextView.setText(Html.fromHtml("<p>something", HtmlCompat.FROM_HTML_MODE_LEGACY)
How can I do this with Text from Jetpack compose?
Same answer as Yhondri, but using HtmlCompat if you are targeting api >24:
#Composable
fun Html(text: String) {
AndroidView(factory = { context ->
TextView(context).apply {
setText(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY))
}
})
}
I have done it this way instead of using TextView in AndroidView and it seems to work quite well for me. The below composable also wraps up the text and expands when you click on it.
#Composable
fun ExpandingText(
description: String,
modifier: Modifier = Modifier,
textStyle: TextStyle = MaterialTheme.typography.body2,
expandable: Boolean = true,
collapsedMaxLines: Int = 3,
expandedMaxLines: Int = Int.MAX_VALUE,
) {
val text = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(description, Html.FROM_HTML_MODE_LEGACY)
} else {
HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
var canTextExpand by remember(text) { mutableStateOf(true) }
var expanded by remember { mutableStateOf(false) }
val interactionSource = remember { MutableInteractionSource() }
Text(
text = text.toString(),
style = textStyle,
overflow = TextOverflow.Ellipsis,
maxLines = if (expanded) expandedMaxLines else collapsedMaxLines,
modifier = Modifier
.clickable(
enabled = expandable && canTextExpand,
onClick = { expanded = !expanded },
indication = rememberRipple(bounded = true),
interactionSource = interactionSource,
)
.animateContentSize(animationSpec = spring())
.then(modifier),
onTextLayout = {
if (!expanded) {
canTextExpand = it.hasVisualOverflow
}
}
)
}
Unfortunately, Jetpack compose does NOT support HTML yet...
So, what you could do is:
Option 1: Create your own HTML parser
Jetpack compose supports basic styling such as Bold, color, font etc.. So what you can do is loop through the original HTML text and apply text style manually.
Option 2: Integrate the old TextView into your Jetpack compose.
Please read: Adopting Compose in your app
Thanks.
You can integrate the old TextView into your Jetpack compose like follows:
AndroidView(factory = { context ->
TextView(context).apply {
text = Html.fromHtml(your_html)
}
})
More info: https://foso.github.io/Jetpack-Compose-Playground/viewinterop/androidview/
you can use the code below:
#Composable
private fun TextHtml() {
Text(text = buildAnnotatedString {
withStyle(style = SpanStyle(color = Gray600)) {
append("normal text")
}
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold,color = Gray700)) {
append("bold text ")
}
})
}
use withStyle to apply the html tags and use append() inside it to add the string

Using Custom Views with Jetpack Compose

Let's suppose I'm using some library that's intended to provide some UI Widgets.
Let's say this library provides a Button Widget called FancyButton.
In the other hand, I have a new project created with Android Studio 4 that allows me to create a new project with an Empty Compose Activity.
The question is:
How should I add this FancyButton to the view stack? Is it possible? Or with Jetpack Compose I can only use components that had been developed specifically for Jetpack Compose. In this case, AFAIK I could only use Android standars components (Text, MaterialTheme, etc).
If I try to use something like this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Greeting("Android")
FancyButton(context, "Some text")
}
}
}
then I get this error:
e: Supertypes of the following classes cannot be resolved. Please make sure you have the required dependencies in the classpath.
Currently (as of 0.1.0-dev04), there is not a good solution to this. In the future, you'll be able to simply call it as FancyButton("Some text") (no context needed).
You can see a demo of what it will look like in the Compose code here.
Update in alpha 06
It is possible to import Android View instances in a composable.
Use ContextAmbient.current as the context parameter for the View.
Column(modifier = Modifier.padding(16.dp)) {
// CustomView using Object
MyCustomView(context = ContextAmbient.current)
// If the state updates
AndroidView(viewBlock = ::CustomView, modifier = modifier) { customView ->
// Modify the custom view
}
// Using xml resource
AndroidView(resId = R.layout.view_demo)
}
You can wrap your custom view within the AndroidView composable:
#Composable
fun RegularTextView() {
AndroidView(
factory = { context ->
TextView(context).apply {
text = "RegularTextView"
textSize = 34.dp.value
}
},
)
}
And here is how to update your custom view during a recomposition, by using the update parameter:
#Composable
fun RegularTextView() {
var string by remember {
mutableStateOf("RegularTextView")
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
AndroidView(
factory = { context ->
TextView(context).apply {
textSize = 34.dp.value
}
},
update = { textView ->
textView.text = string
}
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
string = "Button clicked"
},
) {
Text(text = "Update text")
}
}
}
#Composable
fun ButtonType1(text: String, onClick: () -> Unit)
{
Button (
modifier=Modifier.fillMaxWidth().height(50.dp),
onClick = onClick,
shape = RoundedCornerShape(5.dp),
border = BorderStroke(3.dp, colorResource(id = R.color.colorPrimaryDark)),
colors = ButtonDefaults.buttonColors(contentColor = Color.White, backgroundColor = colorResource(id = R.color.colorPrimaryDark))
)
{
Text(text = text , color = colorResource(id = R.color.white),
fontFamily = montserrat,
fontWeight = FontWeight.Normal,
fontSize = 15.sp
)
}
}

Categories

Resources