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.
Related
I have a simple recyclerview with items (tips) and a loading spinner at the bottom.
here's how the item count and item view type methods look:
#Override
public int getItemViewType(int position) {
if (position == getItemCount() - 1) { // last position
return LOADING_FOOTER_VIEW_TYPE;
}
else {
return TIP_VIEW_TYPE;
}
}
#Override
public int getItemCount() {
return tips.size() + 1; // + 1 for the loading footer
}
basically, i just have a loading spinner under all my items.
I create the adapter once like so:
public TipsListAdapter(TipsActivity tipsActivity, ArrayList<Tip> tips) {
this.tipsActivity = tipsActivity;
this.tips = tips;
}
and then once i have fetched additional items, i call add like so:
public void addTips(List<Tip> tips) {
// hide the loading footer temporarily
isAdding = true;
notifyItemChanged(getItemCount() - 1);
// insert the new items
int insertPos = this.tips.size(); // this will basically give us the position of the loading spinner
this.tips.addAll(tips);
notifyItemRangeInserted(insertPos, tips.size());
// allow the loading footer to be shown again
isAdding = false;
notifyItemChanged(getItemCount() - 1);
}
What's odd here is that when i do that, the scroll position goes to the very bottom. It almost seems like it followed the loading spinner. This only happens on the first add (i.e. when there is only the loading spinner showing initally). subsequent adds maintains the proper scroll position (the position where the items were inserted).
This doesn't happen if i change notifyItemRangeInserted() to notifyItemRangeChanged() like so:
public void addTips(List<Tip> tips) {
// hide the loading footer temporarily
isAdding = true;
notifyItemChanged(getItemCount() - 1);
// insert the new items
int insertPos = this.tips.size(); // this will basically give us the position of the loading spinner
this.tips.addAll(tips);
notifyItemRangeChanged(insertPos, tips.size());
// allow the loading footer to be shown again
isAdding = false;
notifyItemChanged(getItemCount() - 1);
}
Nor does it happen if i simply call notifyDataSetChanged() like so:
public void addTips(List<Tip> tips) {
this.tips.addAll(tips);
notifyDataSetChanged();
}
Here's the code for setting the adapter in my Activity:
public void setAdapter(#NonNull ArrayList<Tip> tips) {
if (!tips.isEmpty()) { // won't be empty if restoring state
hideProgressBar();
}
tipsList.setAdapter(new TipsListAdapter(this, tips));
}
public void addTips(List<Tip> tips) {
hideProgressBar();
getAdapter().addTips(tips);
restorePageIfNecessary();
}
private TipsListAdapter getAdapter() {
return (TipsListAdapter) tipsList.getAdapter();
}
Note:
I don't manually set scroll position anywhere.
I call setAdapter() in onResume()
addTips() is called after I fetch items from the server
Let me know if you need any additional parts of my code.
This only happens on the first add (i.e. when there is only the loading spinner showing initally). subsequent adds maintains the proper scroll position (the position where the items were inserted).
RecyclerView has built-in behavior when calling the more-specific dataset change methods (like notifyItemRangeInserted() as opposed to notifyDataSetChanged()) that tries to keep the user looking at "the same thing" as before the operation.
When the data set changes, the first item the user can see is prioritized as the "anchor" to keep the user looking at approximately the same thing. If possible, the RecyclerView will try to keep this "anchor" view visible after the adapter update.
On the very first load, the first item (the only item) is the loading indicator. Therefore, when you load the new tips and update the adapter, this behavior will prioritize keeping the loading indicator on-screen. Since the loading indicator is kept at the end of the list, this will scroll the list to the bottom.
On subsequent loads, the first item is not the loading indicator, and it doesn't move. So the RecyclerView will not appear to scroll, since it doesn't have to do so to keep the "anchor" on-screen.
My recommendation is to check insertPos and see if it is zero. If it is, that means this is the first load, so you should update the adapter by calling notifyDataSetChanged() in order to avoid this anchoring behavior. Otherwise, call notifyItemRangeInserted() as you're currently doing.
Remove the setAdapter code from onResume ASAP as you are setting new TipsListAdapter(this, tips);
Every time a new reference of the adapter is created...make field mAdapter and then set it in onCreate . RecyclerView doesnt remember the scrolled position because everytime a new reference of adapter is being created.. onResume gets called infinitely when activity is in running state..
So either you setAdapter in onCreate using new operator to create reference for adapter or,
in onResume use mAdapter field variable reference..
Currently, when I
Launch a new Activity by clicking on AutoCompleteTextView's drop down
Close the launched Activity
AutoCompleteTextView's drop down is hidden.
I would like to preserve AutoCompleteTextView's drop-down state which includes
Drop down should not be hidden when gets back from launched Activity
Drop down's scroll position should be preserved.
I'm not exactly sure the reason why AutoCompleteTextView's dropdown will be hidden when I back from launched Activity. Hence, I had tried 2 things
Change windowSoftInputMode of launched Activity from stateAlwaysHidden to stateUnchanged.
In onActivityResult, when the launched Activity is closed, perform mSearchSrcTextView.showDropDown(); explicitly.
However, I am still facing the issue. The previous scroll position of AutoCompleteTextView's dropdown is not preserved. It is reset back to top of the list.
Here's the screen-shot to better illustrate the problem I am facing.
(Current AutoCompleteTextView's dropdown is scrolled to the end. I click on the last item and launch a new Activity)
(New Activity is launched. Now, I click on the BACK soft key twice, to close the keyboard and then close the Activity)
(Due to the explicit call of mSearchSrcTextView.showDropDown(); in onActivityResult, the drop down is shown again. However, its previous scrolled position is not being preserved. Start of list is being shown instead of end of list)
I was wondering, is there any way to preserved the AutoCompleteTextView's DropDown state, when closing a previous launched Activity?
For AutoCompleteTextView, it has a method called dismissDropDown(). I believe when back from newly launched activity, this function is being triggered. So we workaround this problem by extending AutoCompleteTextView & override it's dismissDropDown().
We add a boolean flag temporaryIgnoreDismissDropDown, to indicate whether to temporarily ignore dismissDropDown.
public class MyAutoCompleteTextView extends AutoCompleteTextView {
private boolean temporaryIgnoreDismissDropDown = false;
.....
#Override
public void dismissDropDown() {
if (this.temporaryIgnoreDismissDropDown) {
this.temporaryIgnoreDismissDropDown = false;
return;
}
super.dismissDropDown();
}
public void setTemporaryIgnoreDismissDropDown(boolean flag) {
this.temporaryIgnoreDismissDropDown = flag;
}
}
Before launching new Activity, we set dismissDropDown to true. After coming back from launched activity, dismissDropDown is called. The override method checks if temporaryIgnoreDismissDropDown is true, just set it to false & do nothing. So the real dismissDropDown is skipped.
// myAutoCompleteTextView is instance of MyAutoCompleteTextView
myAutoCompleteTextView.setTemporaryIgnoreDismissDropDown(true);
// launch new Activity
startActivity(....);
Hope this help, good luck!
After an hour of coding, much trying and a lot of googling around, I've put together a solution that does just what you want. It uses reflection to access the ListView within the Dropdown menu and to access the dropdown state when you leave the activity.
The code for this is kinda long, so I'll walk you through all the parts. Firstly, I have some variables we will need:
boolean wasDropdownOpen;
int oldDropdownY;
Handler handler;
The handler will be neccessary for later, as we have to do a little trick in the onResume() method. Initialize it as usual in your onCreate() method:
handler = new Handler(getMainLooper());
Now, let's get to the tricky part.
You need to call the following method before you start any activity. It can't be done in onPause() since the Dropdown menu is already closed when this method is called. In my test code I've overridden the startActivity() and startActivityForResult() method, and called it there, but you can do this however you like.
private void processBeforeStart() {
ListPopupWindow window = getWindow(textView);
if(window == null) return;
wasDropdownOpen = window.isShowing();
ListView lv = getListView(window);
if(lv == null) return;
View view = lv.getChildAt(0);
oldDropdownY = -view.getTop() + lv.getFirstVisiblePosition() * view.getHeight();
}
This will save your dropdown ListView's state for later. Now, we will load it. This is the onResume() method we will need for this:
#Override
protected void onResume() {
super.onResume();
if (wasDropdownOpen)
textView.showDropDown();
handler.postDelayed(new Runnable() {
#Override
public void run() {
ListView lv = getListView(getWindow(textView));
if (lv != null)
scrollToY(lv, oldDropdownY);
}
}, 150);
}
First of all, let me explain this method. We saved the state if the dropdown was open, so we reopen the menu if it was. Simple. The next part is the scrolling. We need to do this in a Handler because the UI is not yet fully loaded when onResume() is called and therefore the ListView is still inaccessible.
The scrollToY() method you see there is a modified version of the code from this post, as Android's ListView does not have an inbuilt method to set the scroll position as precisely as we want it here.
The implementation of this method is as follows:
private void scrollToY(ListView lv, int position) {
int itemHeight = lv.getChildAt(0).getHeight();
int item = (int) Math.floor(position / itemHeight);
int scroll = (item * itemHeight) - position;
lv.setSelectionFromTop(item, scroll);// Important
}
Now, you've probably seen the getWindow() and getListView() methods I've used above. These are the reflection methods, which we have to use because Android does not expose a public API to access the ListView within the ListPopupWindow of the AutoCompleteTextView. Additionally, the DropDownListView, a subclass of ListView that is actually used within this object, is not visible to the oudside as well, so we have to use Reflection once again.
Here is the implementation of my two helper methods:
private ListView getListView(ListPopupWindow window) {
for (Field field : window.getClass().getDeclaredFields()) {
if (field.getType().getName().equals("android.widget.DropDownListView")) {
field.setAccessible(true);
try {
return (ListView) field.get(window);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return null;
}
private ListPopupWindow getWindow(AutoCompleteTextView tv) {
Class realClass = tv.getClass().getName().contains("support") ? tv.getClass().getSuperclass() : tv.getClass();
for (Field field : realClass.getDeclaredFields()) {
if (field.getType().getName().equals(ListPopupWindow.class.getName())) {
field.setAccessible(true);
try {
return (ListPopupWindow) field.get(tv);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return null;
}
I've tested this on Android O (API level 26) and it works just as you described you want it to work.
I hope that the effort I put into this answer gets me a chance on the Bounty ;-)
It sounds like you've already figured out how to show the drop-down on demand (via showDropDown()), so I'll only address how to restore the scroll position of the dropdown.
You can access the first visible position of the dropdown like this:
autocomplete.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
int firstVisiblePosition = parent.getFirstVisiblePosition();
// save this value somehow
}
});
Save the value of this int however you'd like (in memory, via onSaveInstanceState(), pass it through to the started activity so that it can pass it back via onActivityResult(), etc). Then, wherever you re-show the dropdown, do this:
autocomplete.showDropDown();
autocomplete.setListSelection(firstVisiblePosition);
The shortcoming of this technique is that it makes the item at firstVisiblePosition completely visible, so if it was halfway scrolled out of view, the list position won't be restored perfectly. Unfortunately, I don't believe there's any way to save/restore this partial-view offset.
I have a ListView which adapts data from a cursor. The cursor's position of the record is stored via View.setTag(), so it can be retrieved in response to user events.
public class OrderActivity extends Activity {
private ListView list;
private CursorAdapter adapter;
private SQLiteDatabase database;
public void onResume() {
database = new Database(this).getWriteableDatabase();
// query the db and store the position of the cursor
// as the tag while binding the view in the overridden
// CursorAdapter.bindView()
adapter = new SimpleCursorAdapter( /* code here */ );
list.setAdapter(adapter);
}
public void onPause() {
adapter.changeCursor(null);
database.close();
}
private void onClickRowDumpOrder(View row) {
int newPosition = (Integer) row.getTag();
Cursor cursor = adapter.getCursor();
int originalPosition = cursor.getPosition(); // Throws NPE
cursor.moveToPosition(newPosition);
Log.v("tag", "Description: " + cursor.getString(0));
// restore the cursor
cursor.moveToPosition(originalPosition);
}
}
I store the database and the adapter in instance fields of my Activity to allocate/free resources during the activity lifecycle: I create a new database onResume and close it onPause. Unfortunately, I receive lots of reports from my users that a NPE is thrown in the line outlined in the pseudocode above, but I'm not able to reproduce it.
It seems that cursor is null, but I wonder how this is possible if the method onClickRowDumpOrder can only be called after onResume, since it's the callback to the click event (I set this in the XML layout via android:onClick)
Am I doing something wrong? What API misusage causes the cursor to be null? Is there some documentation describing how cursors and adapters are intended to fit in the activity lifecycle?
UPDATE
I got rid of android:onClick in my XML file and manually set the listener inside SimpleCursorAdapter.bindView. To avoid leaks, I remove the listener either in my custom AbsListView.RecycleListener and in my activity's onPause (I retrieve all views with reclaimViews(List<View>)). This seems to fix the bug, and here is my explanation.
A new View is inflated when my activity first starts
instance#1 of my activity is set as the OnClickListener for that particular View in the View's constructor, when it parses the android:onClick attribute
instance#1 leaves the foreground, thus onPause() sets the cursor to null. Note that this activity is still the listener for the View, because neither the view nor the activity are marked for garbage collection. This means that they are referenced in some cache in the Android classes
instance#2 of my activity is created, and its list view somewhat recycle the already created View. The data is shown correctly, but this view still has the old activity (with a null cursor) as the listener
when user clicks my view, instance#1 handler is called, but it has a null cursor. This causes the NPE
This explanation is realistic, however I didn't find relevant code in Android classes (there is a cache in AbsListView.RecycleBin, but the ListView itself is not reused). Moreover, I've never been able to reproduce this bug, and I solely think my fix works because in the last two days I received no reports (usually I get a few dozens a day)
Are you aware of any code in the Android stack which can validate my assumptions?
you can try this way.
if (cursor.moveToFirst()) {
for (int i = 0; i < cursor.getCount(); i++) {
cursor.moveToPosition(i);
}
}
I think you are creating the cursor at the wrong point in the Activity lifecycle. The ListView sample puts in onCreate() what you have in onResume(). I don't know that it's necessarily harmful, but you may be recreating some things that don't need to be.
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.
This application should have four or more related spinners which should reload when their 'parent' spinner' selection changes - as an example with 2 spinners: houses, and rooms - if you choose a house, the room spinner should reload from the sqlite database.
I have tried two approaches: a MySpinner class that takes a "child" Spinner in its constructor and tells the child to update itself when OnSelectedItem is triggered, like so
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
if (MySpinner.this.mChild.equals(null) == false) {
MySpinner.this.mChild.updateData((int)id);
}
}
the child's updateData is
public void updateData (int parentValue) {
new backgroundTask().execute("create");
}
which background tasks is an AsyncTask to query the sqlite database:
#Override
protected Void doInBackground(String... params) {
Db = new MyDatabase(mContext);
Db.open();
if(params[0] == "create") {
if (mTable.equals("T_room")){
mCursor = mDb.getRooms(mParentValue);
}
}
return null;
}
My second approach has been to create all my spinners directly in the activity.java file. This second approach has me implement one AsyncTask for all 4 or more spinners and choose what to query from the db, based on who calls with what value.
The first approach crashes on the only 'real' line of code in the asynctask, the second approach drives me mad with autosetting spinners and a jumble of ifs in the asynctask.
I'm not a coder by any means, and wonder if someone well versed in object-oriented coding can enlighten me as to what would be good coding behaviour to solve my specific problem (several spinners that update each other on selection.)
This is interesting, at this moment I'm doing something similar. Just keep a reference to the adapter, and inside onItemSelected access the object with adapter.getItem(pos). Then you can use this object to update the second spinner adapter. Just take care of UI threading. I would like to do this in a cleaner way but I don't know how to do it.