I'm attempting to change the color of certain items in a ListView. It crashes with a NullPointerException, I'm not exactly sure why, I think it's because the Adapter hasn't created/added the view to the ListView, so it's trying to retrieve an item that isn't on the array. Whenever there is at least 1 item on the list, I am able to add the colored items perfectly fine. How can I resolve this?
int index = 0;
for(ItemEntry i: tentry) {
adapter.add(i.Name); // Adding to Adapter
adapter.notifyDataSetChanged(); // Telling it I've done so
long time = TimeUnit.MILLISECONDS.toDays(i.Date.getTime() - System.currentTimeMillis());
ListView stuff = (ListView) this.findViewById(R.id.contentsList);
if( time < 0 ) {
stuff.getChildAt(index).setBackgroundColor(Color.RED); // Null exception
} else if( time < 1 ) {
stuff.getChildAt(index).setBackgroundColor(Color.RED); // Null exception
} else if( time < 2 ) {
stuff.getChildAt(index).setBackgroundColor(Color.YELLOW); // Null exception
}
index++;
}
Don't reach into children by index to make UI changes like this. It's begging for trouble:
stuff.getChildAt(index).setBackgroundColor(Color.RED); // Null exception
Instead, make changes to the item views in the getView() of your adapter based on some logic that determines which individual items should have what background color.
You should put this kind of logic in the getView method of your Adapter.
There you can modify the backgroundColor of the item depending on its value.
There are many threads on this topic.
For example take a look here: How can I change the background color of list items based on the data being displayed in each item?
Related
I have a recycler view and there are items in it. Now I am using bubble sorting to rearrange them. But adapter.notifyItemMoved doesn't work properly in the loop (it is slow). Please look at the code below and help, I have added comments to make u understand. One more thing, I can't sort the list in beginning before adapting. So please don't put that as a solution. I know that this is a solution but that will not work in my case.
//rv_all is a recycler view
for (i in 0 until rv_all.childCount)
{
for (j in 0 until rv_all.childCount - 1)
{
/* i have added tag while binding in ViewHolder
itemView.stdate.tag = "date" */
val date1 =
sdf.parse(rv_all.getChildAt(j).findViewWithTag < TextView("date").text.toString())
val date2 =
sdf.parse(rv_all.getChildAt(j + 1).findViewWithTag < TextView("date").text.toString())
if (date1 != null && date2 != null)
{
if (date1.before(date2))
{
//alllist is a list used to adapt to recycler view
Collections.swap(alllist, j, j + 1)
alladapter.notifyItemMoved(j, j + 1)
// problem is here ,it is working fine when not in a loop but in loop it is doing nothing
// i have debugged and used breakpoints and what i saw that alllist is swapping elements but adapter child are not rearranging
}
}
}
}
you are iterating through childrens of RecyclerView (why twice? you arent using i anywhere, this is unefficient)
for (i in 0 until rv_all.childCount) {
for (j in 0 until rv_all.childCount - 1) {
but these are only Views - first child/View is at position 0, second 1 etc. When you scroll a bit down and your RecyclerView first visible item is e.g. 10th in alllist, then still first visible View is at position 0, like always
thus these lines makes no sense:
Collections.swap(alllist,j,j+1)
alladapter.notifyItemMoved(j,j+1)
they always swapping and notifying items at the beggining of array, starting 0, but your RecyclerView can be scrolled a bit down to e.g. 10th item - then above lines are swapping items in alllist, but notifyItemMoved does nothing as RecyclerView doesn't have to redraw first items, they are "scrolled out"
so in short: position of View drawn in RecyclerView != position in data array
you can add "real_position" tag in adapter to every child, then you can still iterate through visible childs/Views, obtain Views with findViewByTag, but swap and notifyItemMoved for positions in data array ("real_position" obtained from tag), not visible childs positions in parent RecyclerView
var realPosition : Integer = rv_all.getChildAt(j).tag as Integer // set in adapter
Collections.swap(alllist, realPosition, realPosition+1)
alladapter.notifyItemMoved(realPosition, realPosition+1)
You can try using notifyDataSetChanged():
class MovieAdapter(...) : RecyclerView.Adapter<MovieViewHolder>() {
var data: List<Movies> = list
set(value) {
field = value
notifyDataSetChanged()
}
...
For a more effient way of refreshing use DiffUtil.ItemCallback<...>(). See the Docs
I want to implement search functionality for my RecyclerView. On text changed i want to change the data that are displayed with this widget. Maybe this question has been asked before or is simple, but I don't know how the change the data that is to be shown...
My RecyclerView is defined as follows:
// 1. get a reference to recyclerView
mRecyclerView = (RecyclerView)findViewById(R.id.recyclerView);
// 2. set layoutManger
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
// 3. create an adapter
mAdapter = new ItemsAdapter(itemsData);
// 4. set adapter
mRecyclerView.setAdapter(mAdapter);
And the data that I am showing is something like:
ItemData itemsData[] = { new ItemData("Mary Richards"),
new ItemData("Tom Brown"),
new ItemData("Lucy London")
};
So when when I want to give the adapter another set of data, another array (with one item for example), what should I do?
If you have stable ids in your adapter, you can get pretty good results (animations) if you create a new array containing the filtered items and call
recyclerView.swapAdapter(newAdapter, false);
Using swapAdapter hints RecyclerView that it can re-use view holders. (vs in setAdapter, it has to recycle all views and re-create because it does not know that the new adapter has the same ViewHolder set with the old adapter).
A better approach would be finding which items are removed and calling notifyItemRemoved(index). Don't forget to actually remove the item. This will let RecyclerView run predictive animations. Assuming you have an Adapter that internally uses an ArrayList, implementation would look like this:
// adapter code
final List<ItemData> mItems = new ArrayList(); //contains your items
public void filterOut(String filter) {
final int size = mItems.size();
for(int i = size - 1; i>= 0; i--) {
if (mItems.get(i).test(filter) == false) {
mItems.remove(i);
notifyItemRemoved(i);
}
}
}
It would perform even better if you can batch notifyItemRemoved calls and use notifyItemRangeRemoved instead. It would look sth like: (not tested)
public void filterOut(String filter) {
final int size = mItems.size();
int batchCount = 0; // continuous # of items that are being removed
for(int i = size - 1; i>= 0; i--) {
if (mItems.get(i).test(filter) == false) {
mItems.remove(i);
batchCount ++;
} else if (batchCount != 0) { // dispatch batch
notifyItemRangeRemoved(i + 1, batchCount);
batchCount = 0;
}
}
// notify for remaining
if (batchCount != 0) { // dispatch remaining
notifyItemRangeRemoved(0, batchCount);
}
}
You need to extend this code to add items that were previously filtered out but now should be visible (e.g. user deletes the filter query) but I think this one should give the basic idea.
Keep in mind that, each notify item call affects the ones after it (which is why I'm traversing the list from end to avoid it). Traversing from end also helps ArrayList's remove method performance (less items to shift).
For example, if you were traversing the list from the beginning and remove the first two items.
You should either call
notifyItemRangeRemoved(0, 2); // 2 items starting from index 0
or if you dispatch them one by one
notifyItemRemoved(0);
notifyItemRemoved(0);//because after the previous one is removed, this item is at position 0
This is my answer - thanks to Ivan Skoric from his site: http://blog.lovelyhq.com/creating-lists-with-recyclerview-in-android/
I created an extra method inside my adapter class:
public void updateList(List<Data> data) {
mData = data;
notifyDataSetChanged();
}
Then each time your data changes, you just call this method passing in your new data and your view should change to reflect it.
Just re-initialize your adapter:
mAdapter = new ItemsAdapter(newItemsData);
or if you only need to remove add a few specific items rather than a whole list:
mAdapter.notifyItemInserted(position);
or
mAdapter.notifyItemRemoved(position);
If you want to change the complete Adapter in the recycler view. you can just simply set by recycler.setAdapter(myAdapter);
It will automatically remove the old adapter from recycler view and replace it with your new adapter.
As ygit answered, swapAdapter is interesting when you have to change the whole content.
But, in my FlexibleAdapter, you can update the items with updateDataSet. You can even configure the adapter to call notifyDataSetChanged or having synchronization animations (enabled by default). That, because notifyDataSetChanged kills all the animations, but it's good to have for big lists.
Please have a look at the description, demoApp and Wiki pages: https://github.com/davideas/FlexibleAdapter
I have a ListView with a custom ArrayAdapter and custom items. These items contain multiple View-element, including a Spinner. This Spinner's ArrayAdapter is set like so:
// Method to set or update the Tags in the Spinner
public void updateTagsSpinner(MyHolder h, Spinner sp){
if(h != null && h.orderedProductItem != null){
// If the given Spinner null, it means we change the OrderedProductItem's Spinner
// Is the given Spinner not null, it means we change the Manage Tag's PopupWindow's Spinner
if(sp == null)
sp = h.spTags;
// We know it's an ArrayAdapter<String> so we just ignore the
// "Unchecked cast from SpinnerAdapter to ArrayAdapter<String>" warning
#SuppressWarnings("unchecked")
ArrayAdapter<String> spAdapt = (ArrayAdapter<String>) sp.getAdapter();
ArrayList<String> tagStrings = Controller.getInstance().getAllTagsWithOrderedProductItem(h.orderedProductItem));
if(tagStrings != null && tagStrings.size() > 0){
if(spAdapt == null){
spAdapt = new ArrayAdapter<String>(ChecklistActivity.this, android.R.layout.simple_spinner_item, tagStrings);
spAdapt.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// ArrayAdapter's setNotifyOnChange is true by default,
// but I set it nonetheless, just in case
spAdapt.setNotifyOnChange(true);
sp.setAdapter(spAdapt);
}
else{
spAdapt.clear();
spAdapt.addAll(tagStrings);
}
}
sp.setSelection(h.orderedProductItem.getSelectedFilter());
}
}
For some reason, every time I scroll down and then up again, my Spinners are completely empty.. And when I click on them I can't even open any Spinners anymore (including the ones that aren't empty) because of a warning:
W/InputEventReceiver(899): Attempted to finish an input event but the input event receiver has already been disposed.
Ok, I found the problem, and it's a pesky one.. That's the reason why I post question/answer as whole to help others with similar problems.
WARNING: adapter.clear() changes the original list that has been used by the ArrayAdapter!!
In my Controller I have a HashMap (allTagsOfProducts) with a key = int productId and a value = ArrayList<String> productTags. In the getAllTagsWithOrderedProductItem-method I use the following:
public ArrayList<String> getAllTagsWithOrderedProductItem(OrderedProductItem opi){
if(opi != null && opi.getProductId() > 0){
// Check if a new Tag has been added / removed
if(getProductById(opi.getProductId()).getTagsHasChanged()){
ArrayList<String> newProductTags = new ArrayList<String>();
... // Do some stuff to fill the newProductTags-List
// Replace the previously saved Tags of this Products with this updated one
allTagsOfProduct.put(opi.getProductId(), newProductTags);
// Set the boolean on false
getProductById(opi.getProductId()).setTagsHasChanged(false);
// And return this update list
return newProductTags;
}
// If nothing is added / removed, just return the list we already saved
else
return allTagsOfProduct.get(opi.getProductId());
}
// If the given OrderedProductItem is null return an empty list
else
return new ArrayList<String>();
}
When I debugged this I found out that when I went into the else to the return allTagsOfProduct.get(opi.getProductId()); the first time, everything works fine, and when I went here again, the saved ArrayList inside the HashMap was empty ([]).. The only reason I could think of was the .clear() method of the ArrayAdapter. So instead I had to use a new ArrayList without the reference to the ArrayList in my Controller's HashMap.
TL;DR: The line ArrayList<String> tagStrings = Controller.getInstance().getAllTagsWithOrderedProductItem(h.orderedProductItem)); in my question post is replaced with:
// We create a new ArrayList here, since spAdapt.clear() changes the original list
// all the way into the Controller's HashMap.. You gotta love the Java references.. >.>
ArrayList<String> tagStrings = new ArrayList<String>(
Controller.getInstance().getAllTagsWithOrderedProductItem(h.orderedProductItem));
PS: The reason why I also couldn't open the non-empty Spinners anymore and it gave me the Warning is because of custom ArrayAdapter's (my ListView's in this case) getView() recycling principle. Click this link for more information of how ListView's getView() recycling works. It re-uses the Item's Spinner for another Item. So even though this new Item's Spinner is filled correctly with the other Item's ArrayList/ArrayAdapter, the Input-EventHandler isn't reset, so I still get this Warning for the non-empty Spinners in the recycling ListView-Item.
I have a list of Items that are "seen" or "not seen" in ArrayList<Item>. If they're not seen I change the background color of the ListView item in my CustomArrayAdapter like this :
if(item.is_seen == null || item.is_seen == 0) {
row.setBackgroundResource(R.color.yellow);
} else {
row.setBackgroundResource(R.color.transparent);
}
Now what I want to do is set all items background to transparent after 3 seconds spent on the page.
I already tried to do something like this:
mScheduledExecutor.schedule(new Runnable() {
#Override
public void run() {
for(int i=0; i<mItems.size(); i++) {
final Item n = mItems.get(i);
if(n.is_seen == null || n.is_seen == 0) {
// update value in db
int isSeen = 1;
updateItem(n._id, isSeen);
// change the color of backgrounds
View view = listViewItem.getChildAt(i);
if(view != null) {
view.setBackgroundResource(R.color.red);
}
}
}
Updating the value in the DB works, but the rest doesn't. But I'd like to avoid to reload the data. I just need the color to change.
I don't have errors, it just does nothing.
I look everywhere for an answer and didn't find one.
Am I wrong since the beginning? Is what I want to achieve even possible?
I thank you in advance for all the help you could give me.
Instead of changing the color of the view like youre doing,
// change the color of backgrounds
View view = listViewItem.getChildAt(i);
if(view != null) {
view.setBackgroundResource(R.color.red);
}
(code above will not work, because the views are recycled in the ListAdapter) update the DATA off which you build your list - add a property to the class you are passing into your ListAdapter, then grab that instance from the list and update that property, you have the position at which it needs to be updated already, so that's easy. Then, call notifyDataSetChanged() on the list. It will not redraw the list if you didn't ADD/REMOVE items from list, but it will update correct view for you. This is the only way to do it - absolutely NO WAY to get to a view corresponding to a specific element in a list after list has been drawn already. Only way is to refresh/redraw the list with notifyDataSetChanged(), followed by refreshDrawableState().
I am trying to generate a list from an object using an ArrayAdapter. The result Looks like:
Item A (3)
Item B (1)
Item C (0)
The number in brackets represents the amount of items that are behind that file. I want to Display an Image whenever there is a 1 in brackets - in this case only for item B.
I have an Attribute Image available that is only set true when the item has a 1.
However, when I'm creating the list, it creates everywhere the Image, except in Item C. I have created a short log to try to understand the reason and found out, that public View getView(int position, View convertView, ViewGroup parent) {
method is called up to 11 times... though the 1st 3 should be enough. when i modified my if clause that it should set imageavailable to false when an item is detected - only the 1st item had a Picture. can anyone help me out? ( i also tryed to make if(imageavailable&number==1) resulting in the same result - 1st 2 have a picture
You should be managing the adapter, not adding logic to getView. You logic for handling the adapter should be in its own method. For example:
ArrayList<Drawable> adapter = new ArrayList<Drawable>();
void constructAdapter(List<Drawable>... drawables, int sizeFilter) {
if (drawables != null) {
for (List<Drawable> l : drawables) {
if (drawable.size() == sizeFilter) {
for (int i = 0; i < sizeFilter) {
adapter.add(l.get(i));
}
}
}
}
}
And then from here you would pass the adapter list into your array adapter.
Note: I used drawable as my example, since you said images. This could be whatever you want, so long as you just change the logic to handle that particular data set.