Spinner - show hint when adapter is empty - android

I know there have been several questions that dealt with the problem how to add the "Select one..." hint for the Spinner before the first selection is made. But that's not my case.
What I need is to display the hint only when the SpinnerAdapter is empty. By default in such case, nothing happens on click (but that is not the major problem), and most of all, the spinner doesn't display any text, so it looks like this, which obviously doesn't feel right:
Any idea how to simply handle this problem? I've come up with 2 possible solutions, but I don't like any of them very much:
If the SpinnerAdapter is empty, hide the Spinner from the layout and display a TextView with the same background as the Spinner instead.
Implement a custom SpinnerAdapter whose getCount() returns 1 instead of 0 if the internal list is empty, and at the same time, have its getView() return a TextView with the required "Empty" message, possibly grey-coloured. But that would require specific checking if the selected item is not the "Empty" one.

You can use this SpinnerWithHintAdapter class below
class SpinnerWithHintAdapter(context: Context, resource: Int = android.R.layout.simple_spinner_dropdown_item) :
ArrayAdapter<Any>(context, resource) {
override fun isEnabled(position: Int): Boolean {
return position != 0
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return (super.getDropDownView(position, convertView, parent) as TextView).apply {
if (position == 0) {
// Set the hint text color gray
setTextColor(Color.GRAY)
} else {
setTextColor(Color.BLACK)
}
}
}
fun attachTo(spinner: Spinner, itemSelectedCallback: ((Any?) -> Unit)? = null) {
spinner.apply {
adapter = this#SpinnerWithHintAdapter
itemSelectedCallback?.let {
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {}
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
val selectedItem = parent?.getItemAtPosition(position)
// If user change the default selection
// First item is disable and it is used for hint
if (position > 0) {
it(selectedItem)
}
}
}
}
}
}
}
How to use? Let's assume I have data class called City
data class City(
val id: Int,
val cityName: String,
val provinceId: Int
) {
/**
* By overriding toString function, you will show the dropdown text correctly
*/
override fun toString(): String {
return cityName
}
}
In the activity, initiate the adapter, add hint(first item), add main items, and finally attach it to your spinner.
SpinnerWithHintAdapter(this#MyActivity)
.apply {
// add hint
add("City")
// add your main items
for (city in cityList) add(city)
// attach this adapter to your spinner
attachTo(yourSpinner) { selectedItem -> // optional item selected listener
selectedItem?.apply {
if (selectedItem is City) {
// do what you want with the selected item
}
}
}
}

Related

Get ArrayIndexOutOfBoundsException: length=10; index=-1 when I try to undo a removal of the RecyclerView element

I have a list of the RecyclerView. And I made a swipe removal. Then I made a Snackbar in MainActivity to undo the removal:
val onSwipe = object : OnSwipe(this) {
override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
when (direction) {
ItemTouchHelper.RIGHT -> {
adapter.removeItem(
viewHolder.absoluteAdapterPosition
)
Snackbar.make(binding.rv, "Deleted", Snackbar.LENGTH_SHORT)
.apply {
setAction("Undo") {
adapter.restoreItem(
viewHolder.absoluteAdapterPosition)
}
show()
}
}
}
}
}
Code in adapter:
fun removeItem(pos: Int) {
listArray.removeAt(pos)
notifyItemRemoved(pos)
}
fun restoreItem(pos: Int) {
listArray.add(pos, listArray[pos])
notifyItemInserted(pos)
}
And when I make the undo operation, my app stops, and I see this in a Logcat:
java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1
at java.util.ArrayList.get(ArrayList.java:439)
at com.example.databaselesson.recyclerView.ExpensesAdapter.restoreItem(ExpensesAdapter.kt:79)
at com.example.databaselesson.MainActivity2$onSwipe$1.onSwiped$lambda-1$lambda-0(MainActivity2.kt:391)
at com.example.databaselesson.MainActivity2$onSwipe$1.$r8$lambda$AhJR3pu-3ynwFvPp66LdaLyFdB0(Unknown Source:0)
at com.example.databaselesson.MainActivity2$onSwipe$1$$ExternalSyntheticLambda0.onClick(Unknown Source:4)
Please, help
If you need more code, please, write, and I will send you it
When you delete the item and do notifyItemRemoved, the ViewHolder being used to display that item is removed from the list. Since it's not displaying anything, its absoluteAdapterPosition is set to NO_POSITION, or -1:
Returns int
The adapter position of the item from RecyclerView's perspective if it still exists in the adapter and bound to a valid item. NO_POSITION if item has been removed from the adapter, notifyDataSetChanged has been called after the last layout pass or the ViewHolder has already been recycled.
So when you tap your UNDO button, that viewholder is going to return -1, which is not a valid index for your data list!
You should probably store the actual position you're removing:
override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
// get the position first, and store that value
val position = viewHolder.absoluteAdapterPosition
when (direction) {
ItemTouchHelper.RIGHT -> {
// using the position we stored
adapter.removeItem(position)
// you don't have to use apply here if you don't want - it's designed
// to be chained (fluent interface where each call returns the Snackbar)
Snackbar.make(binding.rv, "Deleted", Snackbar.LENGTH_SHORT)
// using that fixed position value again
.setAction("Undo") { adapter.restoreItem(position) }
.show()
}
}
}
This way you're removing a specific item position, and if the undo button is hit, you use the same position value to restore it. You're not relying on the state of the ViewHolder that was being used.
Also this:
fun restoreItem(pos: Int) {
listArray.add(pos, listArray[pos])
notifyItemInserted(pos)
}
doesn't seem to restore anything? It just inserts a copy of item pos at the same position. Since your removeItem actually deletes the item from the list, there's no way to get it back unless you store it somewhere. You could have a lastDeletedItem variable that you update in removeItem that restoreItem restores:
var lastDeletedItem: Item? = null
fun removeItem(pos: Int) {
// store the deleted item
lastDeletedItem = listArray[pos]
listArray.removeAt(pos)
notifyItemRemoved(pos)
}
fun restoreItem(pos: Int) {
// restore the last thing that was deleted at this position
lastDeletedItem?.let {
listArray.add(pos, it)
notifyItemInserted(pos)
}
}
But then you have the item that was deleted in one place, and the position in another (the snackbar lambda) so you might want to just put them both together - store the lastDeletedPosition in removeItem and reference that in restoreItem (don't pass pos in), or make restoreItem take a pos and item and fetch the item in your swipe callback, when you store the current adapter position
There are two issues here.
1st: Call viewHolder.absoluteAdapterPosition after notifyItemRemoved shall return -1
This match the exception in your Logcat since it is telling you that you are trying to get index=-1 from listArray.
val onSwipe = object : OnSwipe(this) {
override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
when (direction) {
ItemTouchHelper.RIGHT -> {
adapter.removeItem(
viewHolder.absoluteAdapterPosition //<==Let's say position return 8
)
Snackbar.make(binding.rv, "Deleted", Snackbar.LENGTH_SHORT)
.apply {
setAction("Undo") {
adapter.restoreItem(
viewHolder.absoluteAdapterPosition) //<==Deselected item so it shall return -1
}
show()
}
}
}
}
}
2nd: You haven't cached the item object so it will fail to retrieve the correct data
// Assume that `listArray` = ["A", "B", "C"], `pos` = 1
fun removeItem(pos: Int) {
listArray.removeAt(pos) = ["A", "C"]
notifyItemRemoved(pos)
}
// `listArray` = ["A", "C"], `pos` = 1 (Assume you get the correct target pos)
fun restoreItem(pos: Int) {
listArray.add(pos, listArray[pos]) //`listArray[1]` = "C", listArray = ["A", "C", "C"]
notifyItemInserted(pos)
}
In order to resolve this, you will need to cache both the position and item object in onSwiped call
val onSwipe = object : OnSwipe(this) {
override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
when (direction) {
ItemTouchHelper.RIGHT -> {
val cachedPosition = viewHolder.absoluteAdapterPosition // cache position!
val cachedItem = listArray[cachedPosition] // cache item!
adapter.removeItem(cachedPosition)
Snackbar.make(binding.rv, "Deleted", Snackbar.LENGTH_SHORT)
.apply {
setAction("Undo") {
adapter.restoreItem(cachedPosition, cachedItem)
}
show()
}
}
}
}
}

Android Kotlin Get Value of Selected Spinner Item

I'm using AndroidStudio 4.1.1 and kotlin
I'm having issues trying to get the value of a selected item in a spinner view/item. My first attempt, and many threads say to do it this way, is:
val spinner = findViewById<Spinner>(R.id.wMuscleGroup) as Spinner
val selectedText = spinner.getSelectedItem().toString()
This does not work for me. selectedText shows nothing in a Toast or a println
I also tried this, which I also found many threads for:
val spinner = findViewById<Spinner>(R.id.wMuscleGroup) as Spinner
if (spinner != null) {
val adapter = ArrayAdapter( this, android.R.layout.simple_spinner_item, muscleGroups )
spinner.adapter = adapter
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
val selectedText = muscleGroups[position]
println("Muscle Group selected is $selectedText") // <-- this works
}
override fun onNothingSelected(parent: AdapterView<*>) {
// write code to perform some action
}
}
}
// selectedText is not seen here:
println("Muscle Group selected is $selectedText")
This works and I can see selectedString in my println, in the onItemSelected function inside the block. But, how do I get this value to be seen outside of this block of code? I need to use this value in a class object and a database write. I've tried declaring selectedString outside/above the if (spinner != null), but that does not seem to work either.
Thanks for any help.
Your Spinner does not have a value selected by default. Hence calling spinner.getSelectedItem() returns null and null.toString() returns an empty String.
Your code will work only if the user has selected an option from the Spinner first. You can set a initial value by using spinner.setSelection(position).
If you want to use the selected value outside the selection, you can use a global variable as follows:
val spinner = findViewById<Spinner>(R.id.wMuscleGroup) as Spinner
var selectedText = muscleGroups.first() // as default
if (spinner != null) {
val adapter = ArrayAdapter( this, android.R.layout.simple_spinner_item, muscleGroups )
spinner.adapter = adapter
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
selectedText = muscleGroups[position]
}
override fun onNothingSelected(parent: AdapterView<*>) {
}
}
}
If you want to try something with selectedText, you should use it inside onItemSelected function.

recyclerview-selection: stop auto item deselection when touching to the blank space inside the recyclerview

I am trying to use the recyclerview-selection library in my project. I followed this tutorial:
https://proandroiddev.com/a-guide-to-recyclerview-selection-3ed9f2381504
Everything workes fine. But I have a problem. If I tap/touch any blank space inside the RecyclerView, all the selected elements got deselected! I don't find any method or solution to disable this. What should I do?
I am using implementation 'androidx.recyclerview:recyclerview-selection:1.1.0-rc01' in my project.
Edit 1:
I set the RecyclerView background red to describe my problem. Here, blue items are selected items. If I click any red area, then all the selected items got unselected! The select and deselect should only be done by clicking the items. So, I need to disable this feature (or bug!), that unselect all items!
Example project: https://github.com/ImaginativeShohag/multiselection
Solution is to have an out of context selection item like:
class ItemLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<Long>() {
private val outOfContextSelection = object : ItemDetails<Long>() {
override fun getPosition(): Int = OUT_OF_CONTEXT_POSITION.toInt()
override fun getSelectionKey() = OUT_OF_CONTEXT_POSITION
}
override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? {
recyclerView.findChildViewUnder(e.x, e.y)?.let {
return (recyclerView.getChildViewHolder(it) as?
SelectorBarAdapter.SelectorBarViewHolder)?.itemDetails
}
return outOfContextSelection
}
companion object {
const val OUT_OF_CONTEXT_POSITION = 10000L
}
}
so that when a view which is not one of our clickable elements is clicked we can identify further on a predicate like follows:
class SingleSelectionPredicate : SelectionTracker.SelectionPredicate<Long>() {
override fun canSetStateForKey(key: Long, nextState: Boolean): Boolean {
// warranties that an item can't be unselected on click
// warranties that clicks out of the item's scope are disabled
return nextState && key != ItemLookup.OUT_OF_CONTEXT_POSITION
}
override fun canSetStateAtPosition(position: Int, nextState: Boolean) = true
override fun canSelectMultiple() = false
}
You create your own SelectionTracker.SelectionPredicate<Long>.
Override the method canSetStateForKey(key: Long, nextState: Boolean)
like this:
override fun canSetStateForKey(#NonNull key: Long, nextState: Boolean): Boolean {
rv.findViewHolderForItemId(key)?.let { holder -> adapter.canSetStateForItem(holder as YourItemHolder<YourItem>, nextState)}
return true
}
and in your ViewHolder check if the item is already selected if so return false or vice versa.
Edit:
You can also define a "Selection Hotspot" by inSelectionHotspot(e: MotionEvent) in your ItemDetailsLookup.ItemDetails.
In your ViewHolder you can then check if the TouchEvent is in an area that requires a select or unselect.
for example:
override fun inSelectionHotspot(e: MotionEvent): Boolean {
val rect = Rect()
itemView.getGlobalVisibleRect(rect)
if (rect.contains(e.rawX.toInt(), e.rawY.toInt())) {
// in select region
} else {
// not in select region
}
}

Android Spinner Make Item Clickable but not Selectable

Is it possible to have an item in my spinner which is clickable but not selectable?
The scenario here is that i have a Category spinner and i want the user to input his category himself. So i want the "add Item" selection to appear at the end of the Spinner's list and make it clickable only. Can someone help ?
You can do one thing, in listener if you got last position by default select 0 position and do you click action whatever you want.
yourSpinner?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{
override fun onNothingSelected(parent: AdapterView<*>?) {
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
// if position == n, open dialog here
// and then
yourSpinner?.setSelectablePosition(0);
}
}
you can do like
txtTimeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if(position == last){
txtTimeSpinner.setSelection(0);
}else {
//your code here for selection
}
}
#Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
and array first position is as default like ("Select Time")

RecyclerView adds a "empty" layout item and when I click it the app crashes

I didn't add any data to my RecyclerView but it shows a empty box (the one I styled in the layouts for my data) anyways. It crashes with this errormessage
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
Here is my customAdapter:
class CustomAdapterExercise(var exerciseList: ArrayList<Exercise>, val addList: ArrayList<textAdd>) : RecyclerView.Adapter<CustomAdapterExercise.ViewHolder>() {
val typeAdd = 0
val typeExercise = 1
override fun getItemViewType(position: Int): Int {
if (position == exerciseList.size + 1) {
return typeAdd
}
else{
return typeExercise
}
}
//this method is returning the view for each item in the list
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomAdapterExercise.ViewHolder {
if (viewType == typeExercise) {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.exercise_layout, parent, false)
return ViewHolder(itemView)
} else {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.add_layout, parent, false)
return ViewHolder(itemView)
}
}
//this method is binding the data on the list
override fun onBindViewHolder(holder: CustomAdapterExercise.ViewHolder, position: Int) {
if (holder.itemViewType == typeAdd) {
holder.bindAdd(addList[0])
}
else{
if(position != exerciseList.size){
holder.bindItems(exerciseList[position])
}
}
}
//this method is giving the size of the list
override fun getItemCount(): Int {
return exerciseList.size + 2
}
//the class is hodling the list view
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindItems(Exercise: Exercise) {
var exerciseAmount = itemView.findViewById<TextView>(R.id.exerciseAmount)
if(exerciseAmount != null){
exerciseAmount.text = Exercise.exAmount
}
}
fun bindAdd(textAdd: textAdd){
val addText = itemView.findViewById<TextView>(R.id.addText)
if(addText != null){
addText.text = textAdd.textAdd
}
}
}
}
Even if I add some data it still produces a empty box there and I don't get why.
I wonder how can I stop it from producing a empty box always?
These are issues with calculating the index in RecyclerView:
In getItemCount it should be + 1, instead of + 2, as it only needs to add one additional item for add button.
In getItemViewType position at the end of the list if list length, rather than list lenght +1. This is because position is 0-indexed. So, for example, if you have 5 items, positions 0-4 will be your exercise items, and then position 5 (position == exerciseList.size) will be an add item.
Adding logs in getItemViewType for position and generated view type is helpful for debugging, as it shows which positions are calculated incorrectly very quickly.

Categories

Resources