I have a custom ListView item that has an ImageButton in it, and when I click it, the click events don't come through until the list item as a whole is interacted with, such as scrolling the list or tapping somewhere else on the list item. So for instance, if I click the ImageButton 5 times, nothing will happen, but then when I scroll the list, all 5 click events will come through simultaneously.
After a lot of research, I've come to find this is a common question, but none of the solutions I've found have worked for me so far. This question was quite helpful in learning some of the quirks of view interaction, and most other solutions I found used a similar approach to the accepted answer on that question, but unfortunately none of them worked for my particular situation.
So what I need to happen is to handle the click events for the ImageButton on the list item, as well as the click event for the list item itself. I've tried setting android:focusable="false" and android:focusableInTouchMode="false" on pretty much every view element involved in the display of this list. I've also tried setting these attributes programmatically. I also tried setting android:descendantFocusability="blocksDescendants" on several view elements including the ListView, the LinearLayout of the row item, and the CardView that contains all of this stuff.
The confusing part about all of this, is that I have this working just fine in another Activity of the same app. I have an ImageButton in a custom row layout of a ListView, and the onClick events work just fine for that. So it has to be something with my setup on this Activity. Here's some code:
The getView() of my custom BaseAdapter:
public View getView(int position, View convertView, ViewGroup parent) {
FavoritesItemViewHolder favoriteItem = favoritesList.get(position);
if(convertView == null) {
convertView = favoriteItem.getNewConvertView(parent);
} else {
if(favoriteItem.getConvertView() == null) {
convertView = favoriteItem.getNewConvertView(parent);
} else {
convertView = favoriteItem.getConvertView();
}
}
return convertView;
}
The view inflation of my individual view holders:
public View getNewConvertView(ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.favorites_list_item, parent, false);
TextView favoriteText = (TextView) convertView.findViewById(R.id.favoritesItemText);
favoriteText.setText(itemTitle);
//Here is where I'm setting the OnClickListeners for each row...
ImageButton clearButton = (ImageButton) convertView.findViewById(R.id.favoritesClearButton);
clearButton.setVisibility(View.VISIBLE);
if(itemTitle.equals(context.getResources().getString(R.string.no_favorites))) {
clearButton.setVisibility(View.INVISIBLE);
} else {
clearButton.setVisibility(View.VISIBLE);
clearButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Log.d("CLEAR FAVORITE", "The clear button was just clicked on a favorites item...");
}
});
}
return convertView;
}
And here is the XML for my custom row layouts...
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/favoritesListItemContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="#+id/favoritesItemText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:layout_gravity="center_vertical"
android:textSize="16sp"
android:textColor="#android:color/black"
android:layout_weight="0.9"/>
<ImageButton
android:id="#+id/favoritesClearButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:src="#drawable/ic_clear_x"
android:layout_gravity="center_vertical"
android:scaleType="centerInside"
android:adjustViewBounds="true"
android:background="#android:color/white"
style="?android:attr/borderlessButtonStyle"
android:layout_weight="0.1" />
</LinearLayout>
Any help would be appreciated. I'm just completely stumped here.
UPDATES
In this Activity, I'm using getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); in order to disable a SearchView and some other UI, and when the SearchView receives focus, suddenly my ListView items work as intended. So I'm guessing this has an issue with the ListView or the Window it's in having no focus, and so the click events don't come through until something on the screen receives focus.
What's strange is, when the keyboard is showing on the screen, I can click the list items and their ImageButtons and all the click events are handled correctly. But when I dismiss the keyboard, it breaks again.
There are known problems with ListView interfering with the touch events of views inside each item. You should switch to RecyclerView instead. ListView is seen as essentially deprecated for new apps with RecyclerView becoming its replacement.
Related
I am new to android development. For my learning purpose, I am developing an android application to list the fibonacci numbers in a recycler view. The list gets appended with new numbers as the user scrolls down the recycler view.
The image shows the app displaying the index and respective fibonacci number for the index in the recycler view
This is the layout xml of single item in recycler view.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:clickable="false"
android:focusable="false"
android:padding="5dp">
<TextView
android:id="#+id/info_index"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="3"
android:background="#CDD6D5"
android:gravity="center"
android:maxLines="1" />
<TextView
android:id="#+id/info_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="#CDD6D5"
android:gravity="center"
android:maxLines="1" />
</LinearLayout>
Excerpt from the adaptor class,
public class ViewHolder extends RecyclerView.ViewHolder {
final TextView index;
final TextView value;
ViewHolder(View itemView) {
super(itemView);
index = itemView.findViewById(R.id.info_index);
value = itemView.findViewById(R.id.info_value);
}
}
public void onBindViewHolder(#NonNull ViewHolder holder, final int position) {
final TextView value = holder.value;
final TextView index = holder.index;
value.setOnClickListener(new View.OnClickListener() {
#RequiresApi(api = Build.VERSION_CODES.KITKAT)
#Override
public void onClick(View v) {
if (value.getMaxLines() == 1000) {
value.setMaxLines(1);
notifyItemChanged(position);
} else {
value.setMaxLines(1000);
notifyItemChanged(position);
}
}
});
Log.d(TAG, "position-value:" + String.valueOf(position));
holder.index.setBackgroundColor(Color.WHITE);
holder.index.setText(String.valueOf(position + 1));
holder.value.setBackgroundColor(Color.WHITE);
holder.value.setText(mData.get(position));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
holder.index.setTextAlignment(View.TEXT_ALIGNMENT_TEXT_START);
holder.value.setTextAlignment(View.TEXT_ALIGNMENT_TEXT_START);
}
}
When I click on the value textview, the textview does not expand on the first click, instead I have to press two times to make it expand. The first time only the flickering happens. I have tried disabling the animator for recyclerview, tried re using the same view in the recycler view, but nothings helps.
My requirement is to expand fibonacci-value textview on click. By default it should be 1 line and when clicked it should show the whole content with multiple lines (as many as required).
currently this happens with two clicks. first time flickering and second time expands.
I believe this is a bug in android code. But just want to confirm here for any solutions that I might have missed.
The reason of the flickering is that, each time an item is clicked, just after setting maxLines, you are calling notifyItemChanged (which is the correct thing to do), but as a result, before redrawing the item onBindViewHolder is called again. So, when it is called again, there should be a way to know current max lines for that item.
Besides, if you try adding lots of items and scroll up and down, you'll see more bugs (since viewholders are reused) Thus, it is important to set/reset maxlines for each item inside onBindViewHolder (but outside click listener)
Secondly, DefaultItemAnimator of RecyclerView uses cross-fade animation when an item changes and by default, it creates two viewholders for that position for cross-fading between the two. So, above, you set a clicklistener on your "value" textview and interfere with the textview inside onClick callback. However, when you later click and inform adapter that the item is changed, it binds the second viewholder instance. So when you click, your click is consumed with the previous "value" instance, and right afterwards a new instance is bound and you set a new clicklistener to this second viewholder instance.
This is one of many reasons that interfering with viewholder items inside the click listener is error-prone. Sometimes people solve this kind of problems with setTag/getTag but I think it is similarly error-prone as well.
I think the easiest solution is to use a POJO (plain old java object) for each item and include the maxLine state in this POJO. Something like FibonacciItem with fields such as int: index, String: fibonacciNumber, boolean : expanded. Then you will provide the list as a list of FibonacciItems. And inside your click listener, you'll update maxlines of the clicked item. Something like this:
public void onClick(View v) {
if (mList.get(position).isExpanded()) {
mList.get(position).setExpanded(false);
notifyItemChanged(position);
} else {
mList.get(position).setExpanded(true);
notifyItemChanged(position);
}
}
And inside your onBindViewHolder, (but outside your click listener) you should set max lines for each item according to this value:
if(mList.get(position).isExpanded(){
holder.value.setMaxLines(1);
} else {
holder.value.setMaxLines(1000);
}
This will solve the issue. Besides, we usually use POJOs (or data classes) for each item in a list. It is easier to manage.
I was able to repeat your issue on my device, the issue you have is that your LinearLayout is consuming your click before it reaches your textview.
Include:
android:clickable="false"
android:focusableInTouchMode="false"
android:focusable="false"
Within your LinearLayout like so:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:clickable="false"
android:focusableInTouchMode="false"
android:focusable="false"
android:padding="5dp">
<TextView
android:id="#+id/info_index"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="3"
android:background="#CDD6D5"
android:gravity="center"
android:maxLines="1" />
<TextView
android:id="#+id/info_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="#CDD6D5"
android:gravity="center"
android:maxLines="1" />
</LinearLayout>
Also include:
android:nestedScrollingEnabled="false"
Within your RecyclerView
Originally the number picker worked perfectly, but as soon as I put it in a listview item I was unable to enter numbers. When you select it to edit the text it sometimes brings up a text keyboard and sometimes a number keyboard. When pressing the numbers you can see it registering the letter keyboard is being pressed behind it.
Could this possibly be an issue with having a number picker in a listview item?
I've tried changing several attributes regarding focus and changing the input type of the edit text itself (which is indeed set to number correctly by default) and had no luck.
Number picker list item xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<NumberPicker
android:id="#+id/timePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="32dp" />
<TextView
android:id="#+id/minutesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_toEndOf="#+id/timePicker"
android:layout_centerVertical="true"
android:text="#string/test"
android:textColor="#android:color/white"
android:textSize="14sp" />
</RelativeLayout>
Then using this in the adapter for the listview to get the number picker item:
private View getNumberPickerView(int position, View convertView, ViewGroup parent) {
NumberPickerViewHolder holder;
if (convertView == null) {
convertView = mInflator.inflate(R.layout.number_picker_list_item, parent,false);
holder = new NumberPickerViewHolder();
holder.numberPicker = (NumberPicker) convertView.findViewById(R.id.timePicker);
holder.minutesText = (TextView) convertView.findViewById(R.id.minutesText);
convertView.setTag(holder);
}
else {
holder = (NumberPickerViewHolder) convertView.getTag();
}
if (position == mNumberPickerPos) {
holder.numberPicker.setMinValue(0);
holder.numberPicker.setMaxValue(300);
holder.minutesText.setText(R.string.test);
}
return convertView;
}
NumberPickerViewHolder is just an inner class that is used to reuse the listview items.
Edit: I also have an onValueChange listener for the number picker. I didn't think that would be causing the issue, but I checked anyway. Problem still happens with it removed.
Seems that nobody has an answer for this. What I ended up doing was taking the NumberPicker out of the ListView item and replacing it with an EditText that opened up a dialog box with the NumberPicker in it. It works perfectly since it is no longer part of the ListView item
I am trying to make a listview appear to fade to black towards the top of it. Essentially I want to turn something like this:
in to something like this:
I tried two different approaches to this:
The first one was to override the onDraw() of the ListView, this did not work, my "extra" drawing happened behind the listView. The second approach was to put another view on top of the listView, this looks right, but if the user tries to scroll the list by touching the screen where the view is it does not scroll, it seems like the view consumes the touchevent, so about a third of the list is untouchable.
Any tips on how I can implement this?
You can assign the listitem background in a customised view. In getview (of adapter class) based on the item position. Create a gradient in xml and increase the alpha value based on the position.
This is the peice of code which is working for me.
listItem.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:minHeight="64dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="17sp"
android:textColor="#android:color/white"
android:id="#+id/textView"/>
</RelativeLayout>
ItemAdapter.java
#Override
public View getView(int position, View convertView, ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View rowView = inflater.inflate(rowResourceId, parent, false);
TextView textView = (TextView) rowView.findViewById(R.id.textView);
int id = Integer.parseInt(Ids[position]);
textView.setText(Model.GetbyId(id).name);
// this is the main idea behind the UX look n feel.
rowView.setBackgroundColor(context.getResources().getColor(android.R.color.black));
textView.setAlpha((float) position/Ids.length);
return rowView;
}
Please feel free to use holder design pattern in the getView. This is just a proof of concept.
I have ListAdapter which fills list with TextView of category and RadioButton which is to be checked. So, the problem is that I want user to define which category he wants through List and when he clicks on RadioButton, others should be unchecked and so on. I need to set somehow the ID of this RadioButtons and I'm doing that in getView(...) method in ListAdapter but when it calls for second time that method, he doesn't find that RadioButton and I get error while trying to set ID of this RadioButton view. I was debugging it, and first time it finds the view through method findViewById, but second time it doesn't.
I think I might know the problem for it -> when second time it goes to find the view by ID, it can't find it since I've put already other Identifier on the first call, but how can I set unique identifiers for my RadioButtons in the list then?
Here is my code:
getView:
public View getView(int index, View view, ViewGroup parent) {
if (view == null){
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
view = inflater.inflate(R.layout.category_item, parent, false);
}
ListCategoryItem category = categories.get(index); // categories is list
TextView title = (TextView) view.findViewById(R.id.categoryTitle);
naziv.setText(kategorija.getNazivKategorije());
RadioButton radioBtn = (RadioButton) view.findViewById(R.id.categoryRadioButton);
radioBtn.setId(category.getIdCategory());
return view;
}
and here is my layout for category_item:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="#+id/categoryTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:paddingTop="5dp"
/>
<RadioButton
android:id="#+id/categoryRadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:textSize="15sp"
android:onClick="onRadioButtonClick"/>
So, how to set unique identifier to my RadioButtons in view properly or can I make somehow group of RadioButtons which will care for itself that when other one is checked, others to be unchecked - something how in HTML is solved up.
By using setId(), you are changing the identifier associated with the RadioButton that is used to find it in the view tree. You should probably be using setTag() instead, which is documented here. This method allows you to attach some opaque data object to the view that can be retrieved later using the getTag() method.
Look at the code :-
#Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
if (row == null) {
LayoutInflater inflater = ((Activity) mContext).getLayoutInflater();
row = inflater.inflate(R.layout.country_row, parent, false);
}
return row;
}
The country row XML is :--
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:weightSum="1" >
<TextView
android:id="#+id/nameOfCountry"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.8"
android:text="Country" />
<CheckBox
android:id="#+id/checkBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.2"
android:gravity="right" />
Now my question is once I check few of the check boxes in the list view and the scroll the list view, many other check boxes are automatically checked....
I know there is problem in getView but I'm not able to figure out where....
Also the problem is solved if I don't reuse the convert view. But that is a dumb idea...
Any thoughts.....
That's because the ListView does recycling of views. Essentially, it reuses some views that go off screen to make the new ones on screen to help with performance. There are 2 ways of dealing with this:
Set the values of the views before you return the row. For example, you would set the nameOfCountry and whether or not the checkbox is checked before the return view line. To do this though, you need to keep track of what is checked.
Don't use the convertview and just inflate a new view every time. This may result in a performance hit, but as long as the list isn't too long it shouldn't be too noticable (at least in my experience). Simply igonre the convertview value
You have to set the state of the checkbox explicitly if you reuse an old View.