Problem working with chips programmatically - android

I'm adding chips into chipGropup programatically like this
XML
<com.google.android.material.chip.ChipGroup
android:id="#+id/chipGroup"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:singleSelection="true"
app:selectionRequired="true"
app:singleLine="true">
</com.google.android.material.chip.ChipGroup>
This is the function to add chips and contains the listener
fun addChips(Names: List<String>){
Names.forEach {
val chip = Chip(requireContext())
val drawable = ChipDrawable.createFromAttributes(requireContext(), null, 0, R.style.Widget_MaterialComponents_Chip_Choice)
chip.setChipDrawable(drawable)
chip.text = it
chip.isClickable = true
chip.isCheckable = true
binding.chipGroup.isSingleSelection = true
chip.id = ViewCompat.generateViewId()
binding.chipGroup.addView(chip)
}
binding.chipGroup.forEach { child ->
(child as? Chip)?.setOnCheckedChangeListener { _, _ ->
makeAction()
}
}
}
fun makeAction(){
//val id = binding.chipGroup.checkedChipId //NOT WORKING
val ids = binding.chipGroup.checkedChipIds
val categoryNames = mutableListOf<String>()
ids.forEach { id ->
categoryNames.add(binding.chipGroup.findViewById<Chip>(id).text.toString())
}
viewModel.chipFilter(categoryNames, args.orderToEdit.product.sections.toMutableList())
}
I have two problems.
One is that I can’t use checkedChipId because returns -1, so i need to use checkedChipIds. The strange thing is that I define chipgroup and chips as singleSelection.
The other problem is when I click one chip, entered two times to the setOnCheckedChangeListener
Secondly, is there any way to start the fragment with the first chip selected?
Thanks for all!

Related

Android - Disable Popup being dismissible when Material Exposed Dropdown Menu is open

Currently, I have a Material Exposed Dropdown Menu in a popup window, but when I tap outside the popup window, I don't want it to be dismissed if the Material Exposed Dropdown Menu is open. As for what I have tried so far: I can set a setOnClickListener for the AppCompatAutoCompleteTextView with a boolean flag that changes when it is opened or closed, but this only impact whether the text is clicked in the Material Exposed Dropdown. If I set an onClickListener to the TextInputLayout it only works if I tap the border of the Material Exposed Dropdown Menu. If I setEndIconOnClickListener on arrow, it overrides whatever listener is attached by default and the dropdown menu is no longer opened, and the arrow no longer gets flipped, even if I set the Dropdown menu to open manually. I want a unified way to handle this so that the popup window is not dismissible when the material exposed dropdown menu is open. Not sure how much help it will be, but here is the code I have that is currently handling the menus:
private fun addIngredient(view: View) {
val fragmentWidth = view.width
val units = resources.getStringArray(R.array.add_ingredient_measurement_units)
val volUnits = resources.getStringArray(R.array.volumetric_measurement_units)
val massUnits = resources.getStringArray(R.array.mass_measurement_units)
val layoutInflater: LayoutInflater = context?.applicationContext?.getSystemService(
Context.LAYOUT_INFLATER_SERVICE
) as LayoutInflater
#SuppressLint("InflateParams")
val addInventoryPopup = layoutInflater.inflate(R.layout.add_inventory_popup, null)
val conversionLayoutContainer = addInventoryPopup.findViewById<LinearLayout>(R.id.add_ingredient_conversion_container)
val closeAddInventoryPopupFab = addInventoryPopup.findViewById<FloatingActionButton>(R.id.add_ingredient_close_fab)
val unitsMenu = addInventoryPopup.findViewById<AppCompatAutoCompleteTextView>(R.id.add_ingredient_units_menu)
val arrayAdapter = ArrayAdapter(requireContext(), R.layout.dropdown_menu_item, units)
unitsMenu.setAdapter(arrayAdapter)
unitsMenu.addTextChangedListener {
conversionLayoutContainer.removeAllViews()
val currentText = it.toString()
if (volUnits.contains(currentText) || massUnits.contains(currentText)) {
val conversionLayout = layoutInflater.inflate(R.layout.convert_units, conversionLayoutContainer as ViewGroup)
val conversionHeaderText = conversionLayout.findViewById<TextView>(R.id.conversion_text)
val firstUnitMenu = conversionLayout.findViewById<AppCompatAutoCompleteTextView>(R.id.first_unit_menu)
firstUnitMenu.text = it
val secondUnitMenu = conversionLayout.findViewById<AppCompatAutoCompleteTextView>(R.id.second_unit_menu)
if (volUnits.contains(currentText)) {
conversionHeaderText.text = getString(R.string.vol_conversion_text)
val firstUnitArrayAdapter = ArrayAdapter(requireContext(), R.layout.dropdown_menu_item, volUnits)
val secondUnitArrayAdapter = ArrayAdapter(requireContext(), R.layout.dropdown_menu_item, massUnits)
firstUnitMenu.setAdapter(firstUnitArrayAdapter)
secondUnitMenu.setAdapter(secondUnitArrayAdapter)
} else if (massUnits.contains(currentText)) {
conversionHeaderText.text = getString(R.string.mass_conversion_text)
val firstUnitArrayAdapter = ArrayAdapter(requireContext(), R.layout.dropdown_menu_item, massUnits)
val secondUnitArrayAdapter = ArrayAdapter(requireContext(), R.layout.dropdown_menu_item, volUnits)
firstUnitMenu.setAdapter(firstUnitArrayAdapter)
secondUnitMenu.setAdapter(secondUnitArrayAdapter)
}
} else {
Toast.makeText(requireContext(), "No Unit", Toast.LENGTH_LONG).show()
}
}
val popupWindow = PopupWindow(
addInventoryPopup,
(fragmentWidth * 0.90).toInt(),
ViewGroup.LayoutParams.WRAP_CONTENT,
true
)
closeAddInventoryPopupFab.setOnClickListener { popupWindow.dismiss() }
popupWindow.showAtLocation(view, Gravity.CENTER, 0, 0)
}
and here is the part XML file of the layout I am using to inflate the Popup window that contains the material exposed dropdown menu:
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/add_ingredient_units_menu_container"
style="#style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="54dp"
android:hint="#string/unit">
<androidx.appcompat.widget.AppCompatAutoCompleteTextView
android:id="#+id/add_ingredient_units_menu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:cursorVisible="false"
android:drawablePadding="-12dp"
android:focusable="false"
android:focusableInTouchMode="false"
android:inputType="none"
android:padding="0dp"
android:text="#string/none"
android:textSize="15sp"
android:maxLines="1"/>
</com.google.android.material.textfield.TextInputLayout>
Also my targetSDK is 33 and my min is 26.

How to use MutableStateFlow to search item in list

I am learning kotlin flow in android. I want to basically instant search in my list and filter to show in reyclerview. I searched in google and found this amazing medium post. This post is basically search from google. I want to search item in list and show in reyclerview. Can someone guide me how can I start this. I am explanning in more detail
Suppose I have one SearchBox and one Reyclerview which one item abc one, abc two, xyz one, xyz two... etc.
main image when all data is combine
Scenario 1
when I start typing in SearchBox and enter small a or capital A I want to show only two item matching in recyclerview, look like this
Scenario 2
when I enter any wrong text in SearchBox I want to basically show a text message that not found, look like this
Any guidance would be great. Thanks
I am adding my piece of code
ExploreViewModel.kt
class ExploreViewModel(private var list: ArrayList<Category>) : BaseViewModel() {
val filteredTopics = MutableStateFlow<List<opics>>(emptyList())
var topicSelected: TopicsArea? = TopicsArea.ALL
set(value) {
field = value
handleTopicSelection(field ?: TopicsArea.ALL)
}
private fun handleTopicSelection(value: TopicsArea) {
if (value == TopicsArea.ALL) {
filterAllCategories(true)
} else {
filteredTopics.value = list.firstOrNull { it.topics != null && it.title == value.title }
?.topics?.sortedBy { topic -> topic.title }.orEmpty()
}
}
fun filterAllCategories(isAllCategory: Boolean) {
if (isAllCategory && topicSelected == TopicsArea.ALL && !isFirstItemIsAllCategory()) {
list.add(0, code = TopicsArea.ALL.categoryCode))
} else if (isFirstItemIsAllCategory()) {
list.removeAt(0)
}
filteredTopics.value = list.flatMap { it.topics!! }.distinctBy { topic -> topic.title }.sortedBy { topic -> topic.title }
}
private fun isFirstItemIsAllCategory() = list.firstOrNull()?.code == TopicsArea.ALL
}
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.SearchView
android:id="#+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="16dp"
app:closeIcon="#drawable/ic_cancel"
app:layout_constraintBottom_toTopOf="#+id/exploreScroll"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed" />
<HorizontalScrollView
android:id="#+id/exploreScroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/searchView">
<com.google.android.material.chip.ChipGroup
android:id="#+id/exploreChips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipSpacingHorizontal="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:singleLine="true"
app:singleSelection="true" />
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/exploreList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="20dp"
android:paddingTop="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_default="wrap"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/exploreScroll" />
</androidx.constraintlayout.widget.ConstraintLayout>
Category.kt
#Parcelize
data class Category(
val id: String? = null,
val title: String? = null,
val code: String? = null,
val topics: List<Topics>? = null,
) : Parcelable
Topics.kt
#Parcelize
data class Topics(
val id: String? = null,
val title: String? = null
) : Parcelable
Dummy data and coming from server
fun categoriesList() = listOf(
Categories("21", "physical", listOf(Topics("1", "Abc one"), Topics("2", "Abc Two"))),
Categories("2211", "mind", listOf(Topics("1", "xyz one"), Topics("2", "xyz two"))),
Categories("22131", "motorized", listOf(Topics("1", "xyz three"), Topics("2", "xyz four"))),
)
In my view model list is holding above dummy data. And In my recyclerview I am passing the whole object and I am doing flatMap to combine all data into list. Make sure In recyclerview is using Topic and using title property. In Image Abc one, Abc two is holding in Topic. Thanks
After #Tenfour04 suggestion I will go to A2 suggestion because I have already data which converted into flow and passing in my adapter. I am adding my activity code as well.
ExploreActivity.kt
class ExploreActivity : AppCompatActivity() {
private val binding by lazy { ExploreLayoutBinding.inflate(layoutInflater) }
val viewModel by viewModel<ExploreViewModel> {
val list = intent?.getParcelableArrayListExtra(LIST_KEY) ?: emptyList<Category>()
parametersOf(list)
}
var exploreAdapter = ExploreAdapter { topic -> handleNextActivity(topic) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setupView()
}
fun setupView() {
setupSearchView()
setupFilteredTopic()
setupExploreAdapter()
}
private fun setupFilteredTopic() {
lifecycleScope.launchWhenCreated {
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.filteredTopics.collect { filteredTopicsList ->
exploreAdapter.submitList(filteredTopicsList)
}
}
}
}
fun setupSearchView() {
binding.searchView.apply {
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?) = false
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
})
}
}
fun setupExploreAdapter() {
with(binding.exploreList) {
adapter = exploreAdapter
}
}
}
UPDATE 2
ExploreViewModel.kt
val filteredCategories = query
.debounce(200) // low debounce because we are just filtering local data
.distinctUntilChanged()
.combine(filteredTopics) { queryText, categoriesList ->
val criteria = queryText.lowercase()
if (criteria.isEmpty()) {
return#combine filteredTopics
} else {
categoriesList.filter { category -> category.title?.lowercase()?.let { criteria.contains(it) } == true }
}
}
I am getting error when I set in adapter
fixed
filteredTopics.value
The tutorial you linked has a Flow produced by the SearchView. If you want to keep the search functionality in your ViewModel, you can put a MutableStateFlow in your ViewModel that will be updated by the SearchView indirectly. You can expose a property for updating the query.
There are two different ways this could be done, depending on whether you (A) already have a complete list of your data that you want to query quickly or (B) you want to query a server or your database every time your query text changes.
And then even (A) can be broken up into: (A1) you have a static plain old List, or (A2) your source List comes from a Flow, such as a returned Room flow that is not based on query parameters.
All code below is in the ViewModel class.
A1:
private val allCategories = categoriesList()
private val query = MutableStateFlow("")
// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
get() = query.value
set(value) { query.value = value }
// This is the flow that should be observed for the updated list that
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
.debounce(200) // low debounce because we are just filtering local data
.distinctUntilChanged()
.map {
val criteria = it.lowercase()
allCategories.filter { category -> criteria in category.title.lowercase }
}
A2:
In this example I put a simple placeholder flow for the upstream server query. This could be any flow.
private val allCategories = flow {
categoriesList()
}
private val query = MutableStateFlow("")
// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
get() = query.value
set(value) { query.value = value }
// This is the flow that should be observed for the updated list that
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
.debounce(200) // low debounce because we are just filtering local data
.distinctUntilChanged()
.combine(allCategories) { queryText, categoriesList ->
val criteria = queryText.lowercase()
categoriesList.filter { category -> criteria in category.title.lowercase }
}
B
private val query = MutableStateFlow("")
// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
get() = query.value
set(value) { query.value = value }
// This is the flow that should be observed for the updated list that
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
.debounce(500) // maybe bigger to avoid too many queries
.distinctUntilChanged()
.map {
val criteria = it.lowercase()
categoriesList(criteria) // up to you to implement this depending on source
}

How to disable simultaneous clicks on multiple items in Jetpack Compose List / Column / Row (out of the box debounce?)

I have implemented a column of buttons in jetpack compose. We realized it is possible to click multiple items at once (with multiple fingers for example), and we would like to disable this feature.
Is there an out of the box way to disable multiple simultaneous clicks on children composables by using a parent column modifier?
Here is an example of the current state of my ui, notice there are two selected items and two unselected items.
Here is some code of how it is implemented (stripped down)
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(nestedScrollParams.childScrollState),
) {
viewDataList.forEachIndexed { index, viewData ->
Row(modifier = modifier.fillMaxWidth()
.height(dimensionResource(id = 48.dp)
.background(colorResource(id = R.color.large_button_background))
.clickable { onClick(viewData) },
verticalAlignment = Alignment.CenterVertically
) {
//Internal composables, etc
}
}
Check this solution. It has similar behavior to splitMotionEvents="false" flag. Use this extension with your Column modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import kotlinx.coroutines.coroutineScope
fun Modifier.disableSplitMotionEvents() =
pointerInput(Unit) {
coroutineScope {
var currentId: Long = -1L
awaitPointerEventScope {
while (true) {
awaitPointerEvent(PointerEventPass.Initial).changes.forEach { pointerInfo ->
when {
pointerInfo.pressed && currentId == -1L -> currentId = pointerInfo.id.value
pointerInfo.pressed.not() && currentId == pointerInfo.id.value -> currentId = -1
pointerInfo.id.value != currentId && currentId != -1L -> pointerInfo.consume()
else -> Unit
}
}
}
}
}
}
Here are four solutions:
Click Debounce (ViewModel)r
For this, you need to use a viewmodel. The viewmodel handles the click event. You should pass in some id (or data) that identifies the item being clicked. In your example, you could pass an id that you assign to each item (such as a button id):
// IMPORTANT: Make sure to import kotlinx.coroutines.flow.collect
class MyViewModel : ViewModel() {
val debounceState = MutableStateFlow<String?>(null)
init {
viewModelScope.launch {
debounceState
.debounce(300)
.collect { buttonId ->
if (buttonId != null) {
when (buttonId) {
ButtonIds.Support -> displaySupport()
ButtonIds.About -> displayAbout()
ButtonIds.TermsAndService -> displayTermsAndService()
ButtonIds.Privacy -> displayPrivacy()
}
}
}
}
}
fun onItemClick(buttonId: String) {
debounceState.value = buttonId
}
}
object ButtonIds {
const val Support = "support"
const val About = "about"
const val TermsAndService = "termsAndService"
const val Privacy = "privacy"
}
The debouncer ignores any clicks that come in within 500 milliseconds of the last one received. I've tested this and it works. You'll never be able to click more than one item at a time. Although you can touch two at a time and both will be highlighted, only the first one you touch will generate the click handler.
Click Debouncer (Modifier)
This is another take on the click debouncer but is designed to be used as a Modifier. This is probably the one you will want to use the most. Most apps will make the use of scrolling lists that let you tap on a list item. If you quickly tap on an item multiple times, the code in the clickable modifier will execute multiple times. This can be a nuisance. While users normally won't tap multiple times, I've seen even accidental double clicks trigger the clickable twice. Since you want to avoid this throughout your app on not just lists but buttons as well, you probably should use a custom modifier that lets you fix this issue without having to resort to the viewmodel approach shown above.
Create a custom modifier. I've named it onClick:
fun Modifier.onClick(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
) = composed(
inspectorInfo = debugInspectorInfo {
name = "clickable"
properties["enabled"] = enabled
properties["onClickLabel"] = onClickLabel
properties["role"] = role
properties["onClick"] = onClick
}
) {
Modifier.clickable(
enabled = enabled,
onClickLabel = onClickLabel,
onClick = {
App.debounceClicks {
onClick.invoke()
}
},
role = role,
indication = LocalIndication.current,
interactionSource = remember { MutableInteractionSource() }
)
}
You'll notice that in the code above, I'm using App.debounceClicks. This of course doesn't exist in your app. You need to create this function somewhere in your app where it is globally accessible. This could be a singleton object. In my code, I use a class that inherits from Application, as this is what gets instantiated when the app starts:
class App : Application() {
override fun onCreate() {
super.onCreate()
}
companion object {
private val debounceState = MutableStateFlow { }
init {
GlobalScope.launch(Dispatchers.Main) {
// IMPORTANT: Make sure to import kotlinx.coroutines.flow.collect
debounceState
.debounce(300)
.collect { onClick ->
onClick.invoke()
}
}
}
fun debounceClicks(onClick: () -> Unit) {
debounceState.value = onClick
}
}
}
Don't forget to include the name of your class in your AndroidManifest:
<application
android:name=".App"
Now instead of using clickable, use onClick instead:
Text("Do Something", modifier = Modifier.onClick { })
Globally disable multi-touch
In your main activity, override dispatchTouchEvent:
class MainActivity : AppCompatActivity() {
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return ev?.getPointerCount() == 1 && super.dispatchTouchEvent(ev)
}
}
This disables multi-touch globally. If your app has a Google Maps, you will want to add some code to to dispatchTouchEvent to make sure it remains enabled when the screen showing the map is visible. Users will use two fingers to zoom on a map and that requires multi-touch enabled.
State Managed Click Handler
Use a single click event handler that stores the state of which item is clicked. When the first item calls the click, it sets the state to indicate that the click handler is "in-use". If a second item attempts to call the click handler and "in-use" is set to true, it just returns without performing the handler's code. This is essentially the equivalent of a synchronous handler but instead of blocking, any further calls just get ignored.
The most simple approach that I found for this issue is to save the click state for each Item on the list, and update the state to 'true' if an item is clicked.
NOTE: Using this approach works properly only in a use-case where the list will be re-composed after the click handling; for example navigating to another Screen when the item click is performed.
Otherwise if you stay in the same Composable and try to click another item, the second click will be ignored and so on.
for example:
#Composable
fun MyList() {
// Save the click state in a MutableState
val isClicked = remember {
mutableStateOf(false)
}
LazyColumn {
items(10) {
ListItem(index = "$it", state = isClicked) {
// Handle the click
}
}
}
}
ListItem Composable:
#Composable
fun ListItem(
index: String,
state: MutableState<Boolean>,
onClick: () -> Unit
) {
Text(
text = "Item $index",
modifier = Modifier
.clickable {
// If the state is true, escape the function
if (state.value)
return#clickable
// else, call onClick block
onClick()
state.value = true
}
)
}
Trying to turn off multi-touch, or adding single click to the modifier, is not flexible enough. I borrowed the idea from #Johann‘s code. Instead of disabling at the app level, I can call it only when I need to disable it.
Here is an Alternative solution:
class ClickHelper private constructor() {
private val now: Long
get() = System.currentTimeMillis()
private var lastEventTimeMs: Long = 0
fun clickOnce(event: () -> Unit) {
if (now - lastEventTimeMs >= 300L) {
event.invoke()
}
lastEventTimeMs = now
}
companion object {
#Volatile
private var instance: ClickHelper? = null
fun getInstance() =
instance ?: synchronized(this) {
instance ?: ClickHelper().also { instance = it }
}
}
}
then you can use it anywhere you want:
Button(onClick = { ClickHelper.getInstance().clickOnce {
// Handle the click
} } ) { }
or:
Text(modifier = Modifier.clickable { ClickHelper.getInstance().clickOnce {
// Handle the click
} } ) { }
fun singleClick(onClick: () -> Unit): () -> Unit {
var latest: Long = 0
return {
val now = System.currentTimeMillis()
if (now - latest >= 300) {
onClick()
latest = now
}
}
}
Then you can use
Button(onClick = singleClick {
// TODO
})
Here is my solution.
It's based on https://stackoverflow.com/a/69914674/7011814
by I don't use GlobalScope (here is an explanation why) and I don't use MutableStateFlow as well (because its combination with GlobalScope may cause a potential memory leak).
Here is a head stone of the solution:
#OptIn(FlowPreview::class)
#Composable
fun <T>multipleEventsCutter(
content: #Composable (MultipleEventsCutterManager) -> T
) : T {
val debounceState = remember {
MutableSharedFlow<() -> Unit>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
}
val result = content(
object : MultipleEventsCutterManager {
override fun processEvent(event: () -> Unit) {
debounceState.tryEmit(event)
}
}
)
LaunchedEffect(true) {
debounceState
.debounce(CLICK_COLLAPSING_INTERVAL)
.collect { onClick ->
onClick.invoke()
}
}
return result
}
#OptIn(FlowPreview::class)
#Composable
fun MultipleEventsCutter(
content: #Composable (MultipleEventsCutterManager) -> Unit
) {
multipleEventsCutter(content)
}
The first function can be used as a wrapper around your code like this:
MultipleEventsCutter { multipleEventsCutterManager ->
Button(
onClick = { multipleClicksCutter.processEvent(onClick) },
...
) {
...
}
}
And you can use the second one to create your own modifier, like next one:
fun Modifier.clickableSingle(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
) = composed(
inspectorInfo = debugInspectorInfo {
name = "clickable"
properties["enabled"] = enabled
properties["onClickLabel"] = onClickLabel
properties["role"] = role
properties["onClick"] = onClick
}
) {
multipleEventsCutter { manager ->
Modifier.clickable(
enabled = enabled,
onClickLabel = onClickLabel,
onClick = { manager.processEvent { onClick() } },
role = role,
indication = LocalIndication.current,
interactionSource = remember { MutableInteractionSource() }
)
}
}
Just add two lines in your styles. This will disable multitouch in whole application:
<style name="AppTheme" parent="...">
...
<item name="android:windowEnableSplitTouch">false</item>
<item name="android:splitMotionEvents">false</item>
</style>

constraintlayout.widget.Group animation not working with TransitionManager

does anyone has any idea why animating constraintlayout.widget.Group visibility with TransitionManager is not working? Isn't this widget made for these kind of things?
It is working if hiding or showing items after separating views from Group
<androidx.constraintlayout.widget.Group
android:id="#+id/cardHeadersGroup"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="invisible"
app:constraint_referenced_ids="cardSystemHeader,cardSimpleHeader,cardCombinedHeader"
app:layout_constraintBottom_toBottomOf="#+id/cardCombinedHeader"
app:layout_constraintEnd_toEndOf="#+id/cardSystemHeader"
app:layout_constraintStart_toStartOf="#+id/cardSimpleHeader"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible"/>
val headersGroup = binding.cardHeadersGroup
val slideIn = Slide()
slideIn.slideEdge = Gravity.BOTTOM
slideIn.mode = Slide.MODE_IN
slideIn.addTarget(headersGroup)
TransitionManager.beginDelayedTransition(binding.root as ViewGroup, slideIn)
headersGroup.visibility = VISIBLE
I've been recently working with TransitionManager and ConstraintLayout.Group and found it to be very buggy.
Eventually I decided to dump the whole ConstraintLayout.Group and created an in-code AnimationGroup (similar to the in-xml ConstraintLayout.Group):
class AnimationGroup(vararg val views: View) {
var visibility: Int = View.INVISIBLE
set(value) {
views.forEach { it.visibility = value }
field = value
}
}
and an extension function for the Transition:
private fun Transition.addTarget(animationGroup: AnimationGroup) {
animationGroup.views.forEach { viewInGroup -> this.addTarget(viewInGroup) }
}
That way you can do the following (almost exactly the same code, but simpler xml - no ConstraintLayout.Group):
val headersGroup = AnimationGroup(
binding.cardSystemHeader,
binding.cardSimpleHeader,
binding.cardCombinedHeader
)
val slideIn = Slide()
slideIn.slideEdge = Gravity.BOTTOM
slideIn.mode = Slide.MODE_IN
slideIn.addTarget(headersGroup)
TransitionManager.beginDelayedTransition(binding.root as ViewGroup, slideIn)
headersGroup.visibility = VISIBLE
We can also extract the Group's referenced views with simple extension function:
fun Group.getReferencedViews() = referencedIds.map { rootView.findViewById<View>(it) }

Change a value in mutable list in Kotlin

I got this mutablelist:
[Videos(id=4, yt_id=yRPUkDjwr1A, title=test4, likes=0, kat=pranks, ilike=false), Videos(id=3, yt_id=WkyUU9ZDUto, title=test3, likes=0, kat=pranks, ilike=false), Videos(id=2, yt_id=B_X9OQqtduE, title=test2, likes=0, kat=animals, ilike=false), Videos(id=1, yt_id=ywaKlGNiv80, title=test1, likes=0, kat=animals, ilike=false)]
How can I change ilike to true where id is 2
This is what I've tried:
for (i in 0 until vids!!.size) {
Log.d("lets", vids!!.get(i).title)
if(vids!!.get(i).id == 2){
vids!!.get(i).ilike = true
}
}
You can use find function to find the element with id = 2 and change its property:
vids?.find { it.id == 2 }?.iLike = true
Note: it is a good practice to use question mark if the property is nullable and you unsure whether it is null or not.
If you expect few items (maybe 1 or 2?) to be affected,
you can filter the list and then change iLike of the filtered items:
vids!!.filter { it.id == 2 }.forEach { it.iLike = true }
Try this, I'm assuming your Videos structure is a data class defined somewhat like so. data class Videos(val id: Int, val yt_id: String, val title: String, val likes: Int, val kat: String, val ilike: Boolean)
list.forEachIndexed { index, video ->
video.takeIf { it.id == 2}?.let {
list[index] = it.copy(ilike = true)
}
}
I had to change several properties and I had a need to hold the changed object. Therefore following approach worked better for me:
//First, find the position of the video in the list
val videoPosition= list.indexOfFirst {
it.id == 2
}
//Now get your video by position and make changes
val updatedVideo = list[videoPosition].apply {
//Make all changes you need here
ilike = true
//...
}
//Finally, replace updated video into your list.
list[videoPosition] = updatedVideo
Use set to replace the object if you don't want to use predicates or iteration
Eg.
val video = (...,read = true) //or however you are getting the current model
val updatedVideo = video
updatedVideo.read = true
vids[vids.indexOf(video)] = updatedVideo

Categories

Resources