I have a list of questions, and on each item there is a yes and no checkbox. This is created using an abstract class (because there are lots of lists), a child class, and the array adapter. Here is the abstract class code creating the list:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
List<Question> questions = getQuestions(1L);
setContentView(R.layout.activity_questions);
items = (ListView) findViewById(R.id.items);
adapter = new QuestionsAdapter(this, getCurrentContext(), questions, 1L, getDbData());
items.setAdapter(adapter);
}
Here is the QuestionsAdapter:
public View getView(int position, final View convertView, ViewGroup parent) {
View row = convertView;
if (row == null) {
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
row = inflater.inflate(R.layout.row_questions, parent, false);
holder = new QuestionHolder();
holder.question = (TextView) row.findViewById(R.id.question);
holder.yes = (CheckBox) row.findViewById(R.id.yes);
holder.no = (CheckBox) row.findViewById(R.id.no);
row.setTag(holder);
} else {
holder = (QuestionHolder) row.getTag();
}
Question question = questions.get(position);
holder.question.setText(question.getQuestion());
setStateCheckboxes(holder, question);
holder.yes.setTag(getItem(position));
holder.no.setTag(getItem(position));
holder.yes.setOnCheckedChangeListener(listen);
holder.no.setOnCheckedChangeListener(listen);
return row;
}
I have to create the holder to be able to have a listview with checkboxes. Up to this point, everything is working fine.
I then have a view for each element on the list. It is very basic:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dbData = new DbData(this);
this.setContentView(R.layout.single_question);
CheckBox yes = (CheckBox) findViewById(R.id.yes_single);
CheckBox no = (CheckBox) findViewById(R.id.no_single);
}
In this view I can change the status of the checkboxes. This change is reflected in the db, but when I return to the main list, it is only reflected on refresh. I have overridden the onRestart():
#Override
protected void onRestart() {
// Change this
questions = getQuestions(1L);
adapter.notifyDataSetChanged();
super.onRestart();
}
The adapter is pulling the data from the questions ArrayList, so I am repolling it and the notifying the adapter that the data has changed, but this does not change my view. If I refresh the view, then the current state of everything is updated. I know this is a long question, but any help would be appreciated.
Use the onResume() method to call through to notifyDataSetChanged.
You will need code to manage whether the adapter has been created. You don't want to be calling it too early in the process or you run the risk of exceptions.
Generally I do this through a method that is responsible for initialising the adapter and adding it to the listview, which makes it safer to call this irrespective of whether the activity/fragment is starting for the first time or being returned to from another activity (e.g. through the back button), as well as avoiding recreating the adapter or replacing an existing adapter on the listview.
Also better to call the super.onRestart() before your own code.
Related
First of all please correct me if my ideology is incorrect, it is my first time working with fragments and not that good at ListView adapters.
I am currently using DrawerLayout which consist of fragments as you'd expect, I have a FAB which upon clicking will add a new entry to the SQLite db entry and will refresh the ListView (data pulled from the db).
My thinking was that I could get the ListView adapter (which is in Profiles fragment class) in the ActivityMain (as the FAB is not part of the fragment and will have different actions depending on current fragment) and then call .notifyDataSetChanged()
The Profiles fragment class
public class Profiles extends Fragment{
public ProfileListAdapter adapter;
(...)
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DBHandler db = new DBHandler(getActivity().getBaseContext());
adapter = new ProfileListAdapter(getActivity().getBaseContext(), db.getAllProfiles());
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
ListView profileListView = (ListView)view.findViewById(R.id.profileListView);
profileListView.setAdapter(adapter);
}
ProfilesListAdapter (from here)
public class ProfileListAdapter extends ArrayAdapter<Profile> {
private static class ViewHolder {
TextView name;
}
public ProfileListAdapter(Context context, ArrayList<Profile> profileList) {
super(context, R.layout.profile_row, profileList);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// Get the data item for this position
Profile profile = getItem(position);
// Check if an existing view is being reused, otherwise inflate the view
ViewHolder viewHolder; // view lookup cache stored in tag
if (convertView == null) {
// If there's no view to re-use, inflate a brand new view for row
viewHolder = new ViewHolder();
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.profile_row, parent, false);
viewHolder.name = (TextView) convertView.findViewById(R.id.profileListRowValue);
// Cache the viewHolder object inside the fresh view
convertView.setTag(viewHolder);
} else {
// View is being recycled, retrieve the viewHolder object from tag
viewHolder = (ViewHolder) convertView.getTag();
}
// Populate the data into the template view using the data object
viewHolder.name.setText(profile.name);
// Return the completed view to render on screen
return convertView;
}
}
Inside the DBHelper class
// Getting All Profiles
public ArrayList<Profile> getAllProfiles() {
ArrayList<Profile> profileListItems = new ArrayList<Profile>();
// Select All Query
String selectQuery = "SELECT * FROM " + TABLE_PROFILE;
SQLiteDatabase db = this.getWritableDatabase();
Cursor cursor = db.rawQuery(selectQuery, new String[] {});
// looping through all rows and adding to list
if (cursor.moveToLast()) {
do {
Profile profile = new Profile();
profile.setId(Integer.parseInt(cursor.getString(0)));
profile.setName(cursor.getString(1));
// Adding contact to list
profileListItems.add(profile);
} while (cursor.moveToPrevious());
}
// return contact list
cursor.close();
return profileListItems;
}
And this is what I am trying to do upon FAB click controlled in MainActivity, magic number is just an int which starts at 0 and increments by one for me to test the db / ListView easier.
db.addProfile(new Profile("Test Profile " + magicNumber));
magicNumber++;
FragmentManager fm = getSupportFragmentManager();
Profiles fragment = (Profiles) fm.findFragmentById(R.id.fragmentProfiles);
//I gave the fragment xml main FrameLayout an ID
fragment.updateListView();
Inside the Fragment class
public void updateListView(){
adapter.notifyDataSetChanged();
}
I also tried returning the adapter from the Profiles fragment class which I feel is the wrong way to tackle this problem.
public ProfileListAdapter getAdapter(){
return adapter;
}
I always end up with
java.lang.NullPointerException: Attempt to invoke virtual method 'void layout.Profiles.updateListView()' on a null object reference
or
java.lang.NullPointerException: Attempt to invoke virtual method 'com.xxxxx.xxxxx.appName.ProfileListAdapter layout.Profiles.getAdapter()' on a null object reference
To my noobish eye it seems that the adapter cannot be access from outside the fragment Profiles class, not sure if that's what it is - even if so not sure how to work around this. Let me know if any other code / logcat is required. Thank you!
For anyone coming across this question, the solution I found was here: notifyDataSetChange not working from custom adapter (top answer worked well).
Although I did implement CodeCody's idea of putting the FABs in individual fragments I can safely revert to as it was earlier, not sure which option is better.
What do I want to achieve?
In the below SS, when user touches 'vote' button, these vertical progress bars (custom) will be set according to the voting percentages retrieved from server for that particular row.
What is the obstacle?
I have onClickListener inside getView of the CustomAdapter, and when I manipulate the ProgressBar instance (which is in ViewHolder Class), supposingly I want to see the updated ProgressBar on ONLY the one row of the listview that has triggered that action, but, I see every once 3 rows that I scroll down.
Example: I clicked first row, so first row has updated its progress bar, but 4th, 7th, 10th... rows are also updated EVEN IF I don't touch 'vote button'.
My Guessing
I think this problem is related to recycling the view, the weird number is 3 in this case but when I make rows smaller it goes '4', so that is the only clue I have.
SS & Codes
ScreenShot: bit.ly/sofscreenshot
Code:
public View getView(int position, View convertView, ViewGroup parent) {
final ViewHolder holder;
LayoutInflater mInflater = (LayoutInflater) context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
if (convertView == null) {
convertView = mInflater.inflate(layout, null);
holder = new ViewHolder();
//some more initialization
holder.pb1 = (ProgressBar) convertView.findViewById(R.id.leftProgress);
holder.pb2 = (ProgressBar) convertView.findViewById(R.id.rightProgress);
holder.leftVoteButton = (Button) convertView.findViewById(R.id.leftButton);
holder.rightVoteButton = (Button) convertView.findViewById(R.id.rightButton);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.leftVoteButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
holder.pb1.setVisibility(View.VISIBLE);
holder.pb2.setVisibility(View.VISIBLE);
/Some codes...
holder.pb1.setProgress(50);
holder.pb2.setProgress(50);
}
});
}
private class ViewHolder {
//some more objects
ProgressBar pb1;
ProgressBar pb2;
Button leftVoteButton;
Button rightVoteButton;
}
All the answers and comments are appreciated, have a great day and thank you.
You're doing it wrong.
The problem is that you need to have a Model somewhere, and change its status. Then the view is updated regarding the model status.
For example, let's say that this is a "StackOverflow" app, and you have a list of answers. The user upvote the second answer. This means that the second element of the List is upvoted.
Now what?
When the adapter is going through your list of object it will "fire" the getView method for that position. Then you have to update that position according to your model. So, if the position is 1, the adapter is trying to show the second Answer, and you have to set the button to "upvoted". Otherwise you have to set it as "normal".
private List<Answer> answers;
public View getView(int position, View convertView, ViewGroup parent) {
// here get your view (or initialize it)
// get the matching answer
Answer answer = answers.get(position);
if(answer.isUpvoted()) {
holder.pb1.setVisibility(View.VISIBLE);
holder.pb2.setVisibility(View.VISIBLE);
} else {
holder.pb1.setVisibility(View.INVISIBLE);
holder.pb2.setVisibility(View.INVISIBLE);
}
holder.leftVoteButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
holder.pb1.setVisibility(View.VISIBLE);
holder.pb2.setVisibility(View.VISIBLE);
// not sure on how to get this answer here
// you probably have to go "upper" and manage the click from the ListView
answer.setUpvoted(true);
}
});
}
This problem has encountered to me and lots of bodies here but yet i haven't seen an efficient answer.
i have a listview and a delete button for each item of the list. when i click on the button, the item is deleted from the database and also removed from the list but the listview doesn't refresh the items.
i have called the method notifyDataSetChanged() but has no result.(when the item is deleted the title of the next item comes up and it seems there is two item with one title, sorted one after another one).
myListView.invalidateViews() and myListView.invalidate() didn't work too.
whats the exact reason and whats the absolute solution?
Here is my code:
here are the whole codes:
public class AdapterNote extends ArrayAdapter {
public AdapterNote(ArrayList<StructNote> notes) {
super(G.context, R.layout.adapter_note, notes);
}
private class ViewHolder {
TextView txtTitle;
ImageView imgDelete;
public ViewHolder(View view) {
txtTitle = (TextView) view.findViewById(R.id.txtTitle);
imgDelete = (ImageView) view.findViewById(R.id.imgDelete);
}
public void fill(final ArrayAdapter<StructNote> adapter, final StructNote item, final int position) {
txtTitle.setText(item.title + "");
imgDelete.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View arg0) {
G.notes.remove(item);
adapter.remove(item);
deleteDataFromDatabase(item.id);
G.adapter.notifyDataSetChanged();
}
});
}
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
StructNote item = getItem(position);
if (convertView == null) {
convertView = G.inflater.inflate(R.layout.adapter_note, parent, false);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
}
else {
holder = (ViewHolder) convertView.getTag();
}
holder.fill(this, item, position);
return convertView;
}
}
Remove the item from the list or ArrayList belongs to the adapter according to the index of the list and then try to call notifyDataSetChanged()
Its a simple thing that you need to remove it from the adapter by calling yourAdapterObject.remove(POSITION);
Here, yourAdapterObject is the object of the adapter and POSITION is the position you want to remove from listview.
After that you need to refresh the adapter with the remaining data and information. So for that you need to call as below:
yourAdapterObject.notifyDataStateChanged();
In Addition here is the Animation demo given by the developer site for showing animation. But they also shows, how to remove item from list and add it into list. All works fine with nice animation effect.
I guess it will surly help you.
Enjoy Coding... :)
The problem you are facing concerning the 'two items with same title part' has to do with loss of synchronization between the listener and the current position. Try something like this, if your custom item is of type:
Object item
Put it in the view-holder alongside your text-views. After inflating the view, do this:
holder.item = getItem(position);
And then, instead of item.xx do it:
holder.item.xx
Just remove it from your array-list and set adapter to your list view again. do this in OnItemClickListener.
private OnItemClickListener listPairedClickItem = new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
arrlist.remove(2);
lv.setAdapter(adapter);
}
};
int pos = nameList.indexOf(object);
nameList.remove(pos);
listAdapter.notifyDataSetChanged();
Late to the discussion...
When I did it from a class where I use the adapter, the listview just won't refreshed. Even worse, at one point, sometimes it's refreshed, sometimes it's not. And I have called adapter.notifyDataSetChanged();.
Finally I got it work by making a method to delete item directly inside the adapter, like this:
CustomAdapter extends BaseAdapter {
public void deleteItem(AdapterItem ai) {
dataList.remove(ai); /*dataList = data source used by adapter*/
notifyDataSetChanged();
}
}
I'm trying to get a ListView to toggle the background color of its elements when they are selected (this is for selecting songs to add to a playlist). I've been mostly successful, but for some reason whenever the ListView is longer than the screen, selecting either the first or last item also toggles the background of the other. I keep track of whether an item is selected or not in an array of booleans called selectedStatus, and there's not a problem there, so it's purely a UI problem.
Here's the relevant section of the code:
boolean selectedStatus{} = new boolean[songsList.size()];
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
lv = getListView();
// listening for song selection
lv.setOnItemClickListener(new OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
int viewPosition = position - lv.getFirstVisiblePosition();
if(selectedStatus[position]){
selectedStatus[position] = false;
View currentEntry = lv.getChildAt(viewPosition);
currentEntry.setBackgroundResource(R.color.footercolor);
}
else{
selectedStatus[position] = true;
View currentEntry = lv.getChildAt(viewPosition);
currentEntry.setBackgroundResource(R.color.selected);
}
}
});
}
There must be some implementation detail about ListViews that I'm missing, but I can't figure out why this would be happening.
EDIT: I've realized with more testing that it actually links all list elements with positions which are equal mod 12, I just wasn't looking at a long enough list. This seems much less weird, it's just reusing elements, and I'll have to look into this ViewHolder idea.
Since a few people asked, this is all the code I have for making an adapter and populating the list:
// Adding items to ListView
ListAdapter adapter = new ArrayAdapter<String>(getActivity(),
R.layout.playlist_builder_item, songnamesList);
setListAdapter(adapter);
Sounds like you might need to create a proper ListAdapter and implement a ViewHolder.
The ViewHolder avoids the layout reuse that Android ListView implements, so as you can do slightly more complicated things by relying on the Views being the same as before.
You should hold onto the View that you're changing in a static class. For example:
static class ViewHolder {
ImageView backgroundItem;
}
Then in your Adapter's getView method you want to get hold of that ViewHolder and if we are creating a new view, we set it to be new, otherwise we reuse the old view that we have set as a tag to the original.
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if(convertView == null){
// Inflate the view as we normally would
// Create a new ViewHolder
// Set our ImageView to be equal to viewHolder's backgroundItem
// final step
convertView.setTag(viewHolder);
}else{
// use the original ViewHolder that was already saved as a tag
viewHolder = (ViewHolder) convertView.getTag();
}
// Set the background as per your own code
// Return the convertView
return convertView;
}
Don't forget to set your Adapter to your listview by calling the ListView setAdapter(ListAdapter adapter) method
A decent tutorial which incorporates creating an Adapter and a ViewHolder can be found here.
Further reading:
Vogella article on ListViews
Android Developer post on ListViews
I have a weird situation with a custom ArrayAdapter.
When I try to update the adpater with new data, instead of the data being updated, the new data are inserted to the beginning of the listview and the old data are remaining and visible once you scroll the listview.
UPDATE
It seems that the problem is caused by the ArrayList from the fragment bundle.
If I don't set the listview in the onCreateView from the fragment bundle, my update code works fine, but now I'm puzzled why this:
ArrayList<Collection> cityStoresList = fragmentBundle.getParcelableArrayList("stores");
mStoresList.addAll(cityStoresList);
is causing the items to always remain on the list?
END OF UPDATE
Here are parts of the code: (Collection is a custom object model class)
ArrayList<Collection> mStoresList = new ArrayList<Collection>();
/** List Adapter */
private StoresListAdapter mListAdapter;
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
boolean attach = false;
if (container == null) {
attach = true;
}
Bundle fragmentBundle = getArguments();
ArrayList<Collection> cityStoresList = fragmentBundle.getParcelableArrayList("stores");
mStoresList.addAll(cityStoresList);
//inflater code not added here, but is present
mListAdapter = new StoresListAdapter(getActivity(), mStoresList);
mListView.setAdapter(mListAdapter);
return layout;
}
My custom adapter is as follows:
public class StoresListAdapter extends ArrayAdapter<Collection> {
public StoresListAdapter(Context c, ArrayList<Collection> array) {
super(c, 0, array);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// View from recycle
View row = convertView;
// Handle inflation
if (row == null) {
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
row = inflater.inflate(R.layout.row_store, null);
}
// Get the Store
Collection store = getItem(position);
//rest of code follows
return row;
}
}
Now when I want to update my adapter I use the following:
public void updateAdapter(ArrayList<Collection> storesList, final int listIndex) {
mStoresList.clear();
mStoresList.addAll(storesList);
mListAdapter.notifyDataSetChanged();
}
And this creates the issue I mentioned. The new items appear fine, but the previous ones are still visible and added after the new ones.
It's like adding the new items in the ArrayList as the first items, instead of just replacing the old ones.
Any ideas, suggestions?
Ok, finally found the problem.
Because the whole thing is within a fragment, the oncreateView is actually called when I'm attaching the array, so what happens is that my updateAdapter method is called, the items are added and displayed, before the view is actually visible.
Then the oncreateView method is fired and the original bundle items are being added to the Arraylist....