I have a ListFragment that uses a header view. Both the header's contents and the list's are fetched from a background task. In order to not re-fetch the data on configuration changes, I am calling setRetainInstance and keeping the data on the fragment.
When the the configuration changes, the view is recreated, so it removes the header view that I previously populated. Since now I already have the data, I should just re-add the header view to the list.
Unfortunately when I try doing this... boom!
java.lang.IllegalStateException: Cannot add header view to list -- setAdapter
has already been called.
Apparently, even tho the view is destroyed and onCreateView is called again, the list's adapter is already set (or the state is retained), making it impossible to add the header view again.
How can I keep the ListView's header or redraw it without recreating the fragment on orientation changes?
This is intended behaviour, take a look at the Android source code here for guidance on API 17, but really any will do. The relevant part is:
Add a fixed view to appear at the top of the list. If addHeaderView is
called more than once, the views will appear in the order they were
added. Views added using this call can take focus if they want. NOTE:
Call this before calling setAdapter. This is so ListView can wrap the
supplied cursor with one that will also account for header and footer
views.
public void addHeaderView(View v, Object data, boolean isSelectable) {
if (mAdapter != null && ! (mAdapter instanceof HeaderViewListAdapter)) {
throw new IllegalStateException(
"Cannot add header view to list -- setAdapter has already been" +
"called."); // Edit: SK9 wrapped this.
}
FixedViewInfo info = new FixedViewInfo();
info.view = v;
info.data = data;
info.isSelectable = isSelectable;
mHeaderViewInfos.add(info);
// in the case of re-adding a header view, or adding one later on,
// we need to notify the observer
if (mAdapter != null && mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
The adapter is not null when you come to add the header again and an exception is being raised. To resolve your issue, something along the following lines will do just fine:
setListAdapter(null);
getListView().addHeaderView(mHeader);
setListAdapter(new MyAdapter(getActivity(), items));
I wouldn't even classify this as a workaround. I encountered the same problem and this worked for me.
Apparently footers are treated very differently, see here:
public void addFooterView(View v, Object data, boolean isSelectable) {
// NOTE: do not enforce the adapter being null here, since unlike in
// addHeaderView, it was never enforced here, and so existing apps are
// relying on being able to add a footer and then calling setAdapter to
// force creation of the HeaderViewListAdapter wrapper
FixedViewInfo info = new FixedViewInfo();
info.view = v;
info.data = data;
info.isSelectable = isSelectable;
mFooterViewInfos.add(info);
// in the case of re-adding a footer view, or adding one later on,
// we need to notify the observer
if (mAdapter != null && mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
it's a know issue, but you can resolve it like this:
add header before the set adapter and remove him
Yes, it's a known issue, but can be avoided with the proper approach.
It seems that a solution similar to your problem exists.
These guys found a workaround: setSelected in OnItemClick in ListView
Hope it helps ;)
Related
In my app I am using recycler view.I want to show and hide view on particular condition.But when I scroll recycler views I am not getting expected behaviour.When I Visible a view it gets visible for other rows as well randomly.
What I understand is when it recycles it reuses view and when previous view when it gets recycled it finds the visibility of that view.How can I hide view on particular condition? here is my adapter code
#Override
public void onBindViewHolder(UrduRhymesViewHolder holder, int position) {
RhymesModel current = mUrduRhymesList.get(position);
AppUtility.setCustomFont(mContext, holder.tvUrduRhymesName, Constants.HANDLEE_REGULAR);
holder.tvUrduRhymesName.setText(current.getRhymeName());
holder.ivUrduRhymesLogo.setImageUrl(current.getThumbnailUrl(), mImageRequest);
int status = AppUtility.getFavouriteStatus(mContext, current.getRhymeName(), new UrduRhymesDb(mContext));
if (status == 0)
holder.btnFavourite.setBackgroundResource(R.mipmap.btn_star_unactive);
else
holder.btnFavourite.setBackgroundResource(R.mipmap.btn_star);
ProgressbarDetails progressbarDetails = ProgressbarDetails.getProgressDetail(current.getRhymeName());
if (progressbarDetails == null) {
progressbarDetails = new ProgressbarDetails();
progressbarDetails.prgProgressBar = holder.pbRhymeDownload;
progressbarDetails.download_btn_settings = holder.downloadButtonLayout;
} else {
progressbarDetails.prgProgressBar = holder.pbRhymeDownload;
progressbarDetails.download_btn_settings = holder.downloadButtonLayout;
holder.pbRhymeDownload.setProgress(progressbarDetails.progress);
}
ProgressbarDetails.addUpdateProgressDetail(current.getRhymeName(), progressbarDetails);
if (progressbarDetails != null && progressbarDetails.isDownloading) {
Log.e("test","downloading foe position "+position );
holder.downloadButtonLayout.setBackgroundResource(R.mipmap.btn_download);
holder.pbRhymeDownload.setVisibility(View.VISIBLE);
holder.pbRhymeDownload.setProgress(progressbarDetails.progress);
} else {
Log.e("test","should not be visible for position "+position);
holder.pbRhymeDownload.setVisibility(View.GONE);
}
}
Here progressbarDetails.isDownloading (having value true) is the criteria when I want to show my view but is else clause it is not hiding my view
Edit: Here ProgressbarDetails (Singleton )is a class keeping reference of every row of adapter's progress bar.
No direct way of hiding and unhiding recylerview childitems.
Solution:
Let us assume that the recyclerview adapter is ArrayList
Now make another arraylist (temp_list)
Scenarios:
Hide: iterate through your adapter items and remove the ones that you want to hide. Put each of these into temp_list. After iteration is over, call notifyDataSetChanged()
Show: iterate through your temp_list items and remove the ones that you want to show. Put each of these into adapter. After iteration is over, call notifyDataSetChanged()
You should add a flag in your viewHolder that indicates if this view should be displayed or not . and check this flag every time in the onBindViewHolder.
because the recyclerView reuses the same views you should make a decision depending on something special for every view in you viewHolder.
do u mean when your data has been changed?
and your layout want to change ?
adapter.notifyDataSetChanged();
In my OncreateView() set the adapter which is working when i am first loading the page. When i go to another page and make changes then come back to this fragment it is not working adapter.notifyDatasetchanged().
#Override
public void onStart() {
super.onStart();
groupItem.clear();
childItem.clear();
List<String> child_Category;child_Category=new ArrayList<String>();
groupItem = obj_Listdatabase.fetchcategory();
childItem.clear();
ListIterator<String> iterator = groupItem
.listIterator();
while (iterator.hasNext()) {
String categoryname = iterator.next();
child_Category = new ArrayList<String>();
child_Category = obj_Listdatabase
.fetchchildlist(categoryname);
childItem.add(child_Category);
}
adapter.notifyDataSetChanged();
}
Hai I got output for this one:
groupItem.addAll(obj_Listdatabase.fetchcategory());
Instead of This
//groupItem = obj_Listdatabase.fetchcategory();
Because i change the reference in assignment statement(=) for that adapter.So i use addAll() method to store the values only instead of reference.
You should reaaaally indent your code more cleanly, never use several ";" on the same line for example.
And you can definitely use BaseExpandableListAdapter the issu is somewhere else..
You should also notice that onCreate view is more suitable for "creating view" so all the adapter stuff should be somewhere else like onViewCreated, or onActivityCreated as you need (it's just some advices)
I assume your adapter is in a Fragment since you use GetActivity() and in the constructor of your adapter you pass a reference to the activity (context), the group list and the child list.. ok
we ll assume that you are using the fragment onStart. From the official doc :
Called when the Fragment is visible to the user. This is generally tied to Activity.onStart of the containing Activity's lifecycle.
So normally this method is called after onViewCreate, onViewCreated .. etc at least the first time you code is running. So this is ok
Did you try using adapter.notifyDataSetInvalidate() and then do adapter.notidyDataSetChanged ?
One last thing, since you actually pass the data to your adapter by the constructor, when you update your lists how can the adapter be aware of the changes ? Are your lists global (static) ?
If not, before doing adapter.notifyDataSetChanged() you should pass the lists (group and child) to the adapter with some setter..
good luck
I've created a custom ListView by extending AdapterView and I'd like to handle key events inside of this View. I've overridden onKeyDown, but don't ever see it called. I even tried overriding dispatchKeyEvent and that never gets called either. I've also made my View focusable with setFocusable(true).
Also, I'm a bit confused as to which view actually DOES have focus. In HierarchyView, every single view in the tree says hasFocus = false.
How can I receive key events in my View?
Looks like I can answer my own question here.
Taking a look at AdapterView, you can see that it overrides setFocusable with the following implementation:
#Override
public void setFocusable(boolean focusable) {
final T adapter = getAdapter();
final boolean empty = adapter == null || adapter.getCount() == 0;
mDesiredFocusableState = focusable;
if (!focusable) {
mDesiredFocusableInTouchModeState = false;
}
super.setFocusable(focusable && (!empty || isInFilterMode()));
}
As you can see from the final call to super.setFocusable there, the View will not be focusable if there's no data bound yet from the adapter (meaning the adapter is either null or its is <= 0).
So to resolve this, simply add setFocusable(true) at the end of your setAdapter() implementation.
How can I get the current Android view when it displays data that has been updated, and force it to be redrawn? I worked through Android's Notepad tutorial and completed lesson three without any problems — the solution is provided, after all — but I'm stuck on my first non-trivial modification.
I added a new button to the menu, next to the Add note button. When pressed, that button adds a letter to the title of each note in the system. However, the new titles don't show up in the list of notes no matter how long I wait. I know the updater works because the changes do appear if I dismiss the app and bring it back up.
So far, I've discovered that I have to use some kind of invalidation method to make the program redraw itself with the new values. I know that invalidate() is used from the UI thread and postInvalidate() is used from non-UI threads 1, 2, but I don't even know which thread I'm in. Also, both of those methods have to be called from the View object that needs drawing, and I don't know how to obtain that object. Everything I try returns null.
My main class:
public boolean onMenuItemSelected(int featureId, MenuItem item) {
switch(item.getItemId()) {
case INSERT_ID:
createNote();
return true;
case NEW_BUTTON:
expandTitles();
return true;
default:
// Intentionally empty
}
return super.onMenuItemSelected(featureId, item);
}
private void expandTitles() {
View noteListView = null;
// noteListView = findViewById(R.layout.notes_list); // null
// noteListView =
// getWindow().getDecorView().findViewById(android.R.id.content);
// From SO question 4486034
noteListView = findViewById(R.id.body); // Fails
mDbHelper.expandNoteTitles(noteListView);
}
My DAO class:
public void expandNoteTitles(View noteListView) {
Cursor notes = fetchAllNotes();
for(int i = 1; i <= notes.getCount(); i++) {
expandNoteTitle(i);
}
// NPE here when attempt to redraw is not commented out
noteListView.invalidate(); // Analogous to AWT's repaint(). Not working.
// noteListView.postInvalidate(); // Like repaint(). Not working.
}
public void expandNoteTitle(int i) {
Cursor note = fetchNote(i);
long rowId =
note.getLong(note.getColumnIndexOrThrow(NotesDbAdapter.KEY_ROWID));
String title =
note.getString(note.getColumnIndexOrThrow(NotesDbAdapter.KEY_TITLE)) + "W";
String body =
note.getString(note.getColumnIndexOrThrow(NotesDbAdapter.KEY_BODY));
updateNote(rowId, title, body);
}
What do I have to do to get the updated note titles to show up as soon as I press the button?
Obviously, I'm a complete newbie to Android. I point this out to encourage you to use small words and explain even obvious things. I know this is the millionth "Android not redrawing" question, but I've read dozens of existing posts and they either don't apply or don't make sense to me.
1: What does postInvalidate() do?
2: What is the difference between Android's invalidate() and postInvalidate() methods?
According to the tutorial, the list of existing notes are presented in a ListView. That is an adapter based View, so the items it shows are sourced from an adapter extending theBaseAdapter class. In these cases, you should notify the adapter that the contents have changed by calling its notifyDatasetChanged method. This'll signal the ListView to update and redraw its rows.
Edit:
Sorry, I now realize that this example uses CursorAdapters. These source the items to show from a Cursor object that was obtained from a database query. Now, what the notifyDatasetChanged() tells the adapter is, that the data that backs the adapter has changed, so Views that show stuff based on this adapter need to redraw their contents. In the case of a CursorAdapter, this data is coming from a cursor. So you also need to requery that cursor, refreshing it from the DB, like this:
private void expandTitles() {
mDbHelper.expandNoteTitles();
CursorAdapter adapter = (CursorAdapter)getListAdapter();
adapter.getCursor().requery();
}
The requery() method automatically calls the notifyDatasetChanged() in this case, so you don't need to worry about that, the list will update itself. See this thread also: https://groups.google.com/forum/?fromgroups#!topic/android-developers/_FrDcy0KC-w%5B1-25%5D.
I'm having a strange issue with a custom implementation of Android's ArrayAdapter. To give some background, I'm trying update a ListView's contents while preserving the current scroll position.
I have a service which executes a thread to update data that's displayed in the ListView. That data is stored in an ArrayList and that ArrayList is used to generate some custom ArrayAdapters for the ListView. The adapters are also updated when an item in the ListView is pressed (either adding or removing an item). I used to just create new adapters each time there was any type of change and then set this new adapter to the ListView. This worked, but caused the ListView to scroll to the top each time. Given the nature of my application this was undesirable. The current scrolled position in the ListView must be maintained between updates.
Instead of creating new adapters I began clearing the adapter that I needed to update using the adapter's clear() method, then rebuild the adapter's items by using the adapter's add() method. Both of these methods are being called on the adapter. The adapters are all set to notifyDataOnChange in their constructors so I don't have to manually call notiftyDatasetChanged() each time (although given my issue I've tried calling it manually as well to no avail).
Here's what my custom adapter looks like:
public class RealmAdapter extends ArrayAdapter<Realm>
{
Context c;
public RealmAdapter(Context context, int resource, int textViewResourceId)
{
super(context, resource, textViewResourceId);
setNotifyOnChange(true);
c = context;
}
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
...
}
...
}
Long story short, here's my issue. When I call clear() on the adapter, the adapter is not being cleared.
Here's a snippet from my onPostExecute in my thread that does updating. I'm being sure to put it here so it's updating on the UI thread. I also have this exact code copied in a private method in my UI activity. This code does not work in either place:
appState.favoriteAdapter.clear();
Log.d(LOG_TAG, "COUNT: " + appState.favoriteAdapter.getCount());
for(Realm r : appState.favorites) {
appState.favoriteAdapter.add(r);
}
Log.d(LOG_TAG, "COUNT: " + appState.favoriteAdapter.getCount());
As an example, if the above adapter had 3 items in it, calling a getCount() right after the clear() is returning 3 instead of 0. Likewise, if the appState.favorites ArrayList only has 2 items in it, the getCount() after the loop is still returning 3, not 2. Because the adapter is not responding to any of these calls it makes it impossible to update in any fashion. I can post a Logcat later if that will be helpful, but there are no exceptions or anything useful being displayed.
After busting my head for hours, the issue I appear to be having is that the adapter is not responding to calls to any methods that alter it. I've tried passing an empty ArrayList into the adapter's super() call, this does not help. Am I missing something or using the ArrayAdapter incorrectly? I've searched all over and I've already checked a lot of the common problems such as modifying the underlying array and expecting it to update, not calling (or in my casing setting to the adapter) notifyDatasetChanged(), and using an unsupported operation on the underlying collection.
The declaration of the favoriteAdapter is very simple and is contained in my Application class:
public RealmAdapter favoriteAdapter;
Here is the initialization of the favoriteAdapter from above:
if(appState.favoriteAdapter == null) {
appState.favoriteAdapter = new RealmAdapter(c, R.layout.list_item, R.layout.realm_entry, appState.favorites);
}
else {
appState.favoriteAdapter.clear();
Log.d(LOG_TAG, "COUNT: " + appState.favoriteAdapter.getCount());
for(Realm r : appState.favorites) {
appState.favoriteAdapter.add(r);
}
Log.d(LOG_TAG, "COUNT: " + appState.favoriteAdapter.getCount());
}
The above code is in both my UI thread and the thread that downloads the refreshed data.
Underneath the code above a filter is put in place:
if(appState.favoriteAdapter != null && RealmSelector.realmFilter != null) appState.favoriteAdapter.getFilter().filter(RealmSelector.realmFilter.getText().toString());
Would the filter affect clearing the list? Logic would dictate not...
I had filters being applied to the custom ArrayAdapter. Apparently this interferes with adding and removing items from the adapter itself? I added this code to my method and it is now working:
if(appState.favoriteAdapter != null && realmFilter != null) {
appState.favoriteAdapter.getFilter().filter(realmFilter.getText().toString());
}
I'd love if anyone could explain why this matters. I thought filters were meant to select subsets of items in the adapter. In my testing I was leaving the text box that is used for the filter empty, thus no actual filter text should have been applied. Again, if someone knows what's going on and could explain to me why this fixes the problem I'd love to know.