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.
Related
I have added a TextView as an ActionView to the menu items of my NavigationView. I use this TextView to show how many unread elements there are. I followed this article for the implementation: https://medium.com/android-news/android-adding-badge-or-count-to-the-navigation-drawer-84c93af1f4d9
Now I want to make sure the text color of this ActionView always matches the tint of the MenuItem it is attached to.
I have currently implemented it like this:
navController.addOnDestinationChangedListener { _, destination, _ ->
updateBadgesTextColor(destination)
}
// some other code
private fun updateBadgesTextColor(currentDestination: NavDestination) {
val menu = binding.mainNavView.menu
val currentMenuGraph = getMenuParentGraph(currentDestination, menu)
val defaultColor = ContextCompat.getColor(this, R.color.drawer_layout_default_tint_color)
val selectedColor = ContextCompat.getColor(this, R.color.drawer_layout_selected_tint_color)
menu.forEach { menuItem ->
(menuItem.actionView as? TextView)?.setTextColor(
if (menuItem.itemId == currentMenuGraph?.id) {
selectedColor
} else {
defaultColor
}
)
}
}
private fun getMenuParentGraph(destination: NavDestination, menu: Menu): NavGraph? {
var currentGraph: NavGraph? = destination.parent
while (currentGraph != null && menu.findItem(currentGraph.id) == null) {
/*
note: In my app the menu item and the navGraph that are linked always have the same id.
In an app where this isn't the case, this function won't work correctly.
*/
currentGraph = currentGraph.parent
}
return currentGraph
}
While this does do what I want it to do, it isn't at all efficient because it iterates through the entire list of MenuItems every single time the NavDestination changes, even when the NavDestination change doesn't result in a change of selected MenuItem.
Is there a more efficient way to solve this?
You can track the previous selected destination Id and reset it for a new call to addOnDestinationChangedListener callback.
Normally in navigation components, the destination ids should be the same as thoses correspond to the navView menu Ids.
var previousId = -1
navController.addOnDestinationChangedListener { _, destination, _ ->
if (previousId != -1) {
val previous = binding.mainNavView.menu.findItem(previousId).actionView as TextView?
previous?.setTextColor(ResourcesCompat.getColor(resources, R.color.drawer_layout_default_tint_color, null))
}
val current = binding.mainNavView.menu.findItem(destination.id).actionView as TextView?
current.setTextColor(ResourcesCompat.getColor(resources, R.color.drawer_layout_selected_tint_color, null))
}
Make sure to handle saving the previousId permanently to avoid losing it in configuration changes.
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!
I made a custom tab item for my tab layout and initialized it using view binding as follows:
val tabView = CustomTabBinding.inflate(LayoutInflater.from(mContext), null, false)
tabView.tvCustomTabTitle.text = it.title
tabView.tvCustomTabCount.visibility = View.GONE
Now when the user selects/unselects the tab I want to change the appearance of this custom view. Usually I achieved this using kotlin synthetics as follows:
fun setOnSelectView(tabLayout: TabLayout, position: Int = 0) {
val tab = tabLayout.getTabAt(position)
val selected = tab?.customView
if (selected != null)
selected.tv_custom_tab_title?.apply {
setTextColor(mContext.getColorCompat(R.color.colorAccent))
typeface = setFont(true)
}
selected?.tv_custom_tab_count?.apply {
setBackgroundResource(R.drawable.bullet_accent)
mContext.getColorCompat(android.R.color.white)
}
}
But now how do I achieve this using view binding?
I am using the method of findViewById():
fun Context.setOnSelectView(tabLayout: TabLayout, position: Int = 0) {
val tab = tabLayout.getTabAt(position)
val selected = tab?.customView
if (selected != null){
val title = selected.findViewById<TextView>(R.id.tv_custom_tab_title)
val count = selected.findViewById<TextView>(R.id.tv_custom_tab_count)
title.apply {
setTextColor(getColorCompat(R.color.colorAccent))
typeface = setFont(true)
}
count.apply {
setBackgroundResource(R.drawable.bullet_accent)
getColorCompat(android.R.color.white)
}
}
}
but I am hoping there is a better way to do this. If yes, then please do help me out.
Late reply but this is how I used view binding for custom tab layout, hope it helps
custom_tab.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/tv_custom_tab_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="#+id/tv_custom_tab_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
your activity/fragment:
val tab = binding.tabLayout.getTabAt(position)
tab.setCustomView(R.layout.custom_tab)
val tabBinding = tab.customView?.let {
CustomTabBinding.bind(it)
}
tabBinding?.tvCustomTabTitle?.text = "your title here"
Long story short. I want my spinner to always show same first item "Add". When item selected from the list it should be gone from the list and the specific action happen. The text "Add" should still appear on spinner. So my question is how to make that spinner always show first item on his dataList? PS. I made that first item in the list don't show when drop down is open.
My layout:
<Spinner
android:id="#+id/spinner_add"
style="#style/Widget.AppCompat.Spinner.Underlined"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:backgroundTint="#color/colorButton" />
Method to set spinner adapter with data list:
private fun fillAddSpinner() {
val spinner: Spinner = findViewById(R.id.spinner_add)
val titles: MutableList<String> = ArrayList()
titles.add(resources.getString(R.string.add_advanced_filter))
for (filter in tableAdvancedFilters) {
titles.add(filter.title)
}
val dataAdapter = object : ArrayAdapter<String?>(this, R.layout.spinner_item,
titles as List<String?>
) {
override fun getDropDownView(
position: Int,
convertView: View?,
parent: ViewGroup
): View {
var v: View? = null
// If this is the initial dummy entry, make it hidden
if (position == 0) {
val tv = TextView(context)
tv.height = 0
tv.visibility = View.GONE
v = tv
} else { // Pass convertView as null to prevent reuse of special case views
v = super.getDropDownView(position, null, parent)
}
// Hide scroll bar because it appears sometimes unnecessarily, this does not prevent scrolling
parent.isVerticalScrollBarEnabled = false
return v!!
}
}
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = dataAdapter
}
I want it stay the same like it is now:
After user selects one item from spinner I will render specific filter choices.
If you want to programmatically set spinner selected item use the following: spinnerObject.setSelection(INDEX_OF_ITEM)
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) }