My list adapter creates some Views (both normal views and header views) that use constantly-running timers to cycle their displayed image.
I wonder what's the appropriate way to deallocate those timers and other view-related resources, i mean, the best practice to know that an Item View (created by Adapter.getView) is no more visible/needed, or has been recycled, or de-attached, or even their parent widget is not in use.
I'm ideally looking for a method / event / listener in the View itself, rather than the parent Widget having to delegate an event. I'd like to know
When the View is given to the recycler (ie- it has exited screen on a fling, etc)
When the View can be destroyed (ie- the containing widget has been destroyed)
So far I think I haven't found the appropriate, fail-proof event, if any.
I tried View.onAttachedToWindow() / View.onDetachedFromWindow() but, opposed to onAttachedToWindow that seems it's always properly called, onDetachedFromWindow is not. It seems it's only called when a recycled view is about to be reused, but not on other cases like when the View enters the recycling pool (exits screen), or when the widget is destroyed, or even when the activity is finished.
Any suggestion?
EDIT:
I have been doing some tests, and found that onDetachedFromWindow on the parent widget IS reliable. At the moment my solution is to propagate that event to the adapter, which in turn finds the views on screen (getFirstVisiblePosition...) and manually triggers an onDetachedFromWindow with the code below, but, you know, this is very ugly. I'm sure there's a better way.
// ugly function to trigger an onDetachedFromWindow to any view
// leeched from com.tonicartos.widget.stickygridheaders.StickyGridHeadersGridView
public static void detachHeader(View header) {
if (header == null) { return; }
try {
Method method = View.class.getDeclaredMethod("dispatchDetachedFromWindow");
method.setAccessible(true);
method.invoke(header);
} catch (NoSuchMethodException e) {
throw new RuntimePlatformSupportException(e);
} catch (IllegalArgumentException e) {
throw new RuntimePlatformSupportException(e);
} catch (IllegalAccessException e) {
throw new RuntimePlatformSupportException(e);
} catch (InvocationTargetException e) {
throw new RuntimePlatformSupportException(e);
}
}
RecyclerView.Adapter has a method to notify you when a view is recycled. Have a look at
public void onViewRecycled(RecyclerView.ViewHolder holder);
a) Leave the activity - flush everything in the adpater
May a proper way is to use the onStop Method of the surrounding activity. There you do have a reference to the adapter. In the adapter you can provide a flushAllResources() Method which you call from the activity.
This will work for cases where one leaves the activity.
#Override
public void onViewRecycled(#NonNull final MyViewHolder holder) {
flushAllViewRefreshHandlers();
}
b) View is gonna be re-used due to scrolling - flush the old data as the view was used previuously
Additionally in the onViewRecycled Method (as mentioned previously) you should also flush your resources.
This will work for cases where single items/views of the recycler are going to be re-used (recycled) for other entities. In this case you may only flush data of the specific view/entity and not all of the whole adapter.
Related
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 an issue where ListFragment.onListItemClick is called after onDestroyView. I'm getting lots of error reports in the field (10-20 per day of ~1000 active users), but the only way I found to reproduce it is to hammer the back button while clicking all over the screen. Are hundreds of users really doing this?
This is the trace:
java.lang.IllegalStateException: Content view not yet created
at au.com.example.activity.ListFragment.ensureList(ListFragment.java:860)
at au.com.example.activity.ListFragment.getListView(ListFragment.java:695)
at au.com.example.activity.MyFragment.onListItemClick(MyFragment.java:1290)
at au.com.example.activity.ListFragment$2.onItemClick(ListFragment.java:90)
at android.widget.AdapterView.performItemClick(AdapterView.java:301)
at android.widget.AbsListView.performItemClick(AbsListView.java:1519)
at android.widget.AbsListView$PerformClick.run(AbsListView.java:3278)
at android.widget.AbsListView$1.run(AbsListView.java:4327)
at android.os.Handler.handleCallback(Handler.java:725)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:5293)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1102)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:869)
at dalvik.system.NativeStart.main(Native Method)
Caused from calling getListView().getItemAtPosition in MyFragment.onListItemClick (MyFragment:1290). How can getView return null during a click handler callback? I also determined the fragment was detached at this stage, isAdded() was false, and getActivity was null.
One workaround would be to replace getListView with the listView passed in from the callback
public void onListItemClick(ListView listView, View v, int position, long id), but other functions will still need to update other parts of the UI, so this would just move the problem somewhere else.
Instead, I nulled the callback in onDestroyView:
public void onDestroyView() {
mHandler.removeCallbacks(mRequestFocus);
if(mList!=null){
mList.setOnItemClickListener(null);
}
mList = null;
mListShown = false;
mEmptyView = mProgressContainer = mListContainer = null;
mStandardEmptyView = null;
super.onDestroyView();
}
But I still have this onClick problem in other (non-list) fragments too. How exactly does the framework suppress these callbacks normally when the fragment is removed (eg in onBackPressed -> popBackStackImmediate())?
In onDestroyView, I null out extra views that I created in onCreateView. Do I need to manually clear every listener I've set like this?
This is a similar issue to the unanswered q: Fragment's getView() returning null in a OnClickListener callback
I'm using setOnRetainInstance(true) in my fragments, btw.
You really haven't given very much information, but based off what you've given, it sounds like Fragment pending Transactions might be your issue.
In Android, whenever you are changing, or instantiating fragments, it's all done through Pending Transactions unless told to do otherwise. It's essentially a race condition.
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, new ExampleFragment()
.commit();
The UI Thread has a queue of work that it needs to do at any given time. Even though you've committed the FragmentTransaction after running the above code, it's actually been queued on the UI Thread at the end of the queue, to happen after everything that is currently pending has been finished. What this means is that if click events happen while the transaction is pending (which can easily happen, i.e. you spamming clicks on the screen, or clicking with multiple fingers), those click events will be placed on the UI Threads queue after the FragmentTransaction.
The end result is that the Fragment Transaction is processed, your fragment View is destroyed, and then you call getView() and it returns null.
You could try a few things:
getSupportFragmentManager().executePendingTransactions() This will execute all pending transactions right then, and removes the pending aspect
Check to see if the Fragment isVisible() or isAdded() or some other fragment 'is' method that allows you to get runtime information about the current state the Fragment is in it's lifecycle, before you execute code that could potentially be run after the fragments view is destroyed (i.e. click listeners)
So lets say you have a click handler, where when the user clicks something you animate to another fragment. You could use something like the below piece of code that you run before the FragmentTransaction on your outermost view (in a Fragment, it'd be what returns from getView()), and that would either permanently disable clicks to a view if it was going to be destroyed, or temporarily disable clicks for a a period of time if you are going to re-use the view.
Hope this helps.
public class ClickUtil {
/**
* Disables any clicks inside the given given view.
*
* #param view The view to iterate over and disable all clicks.
*/
public static void disable(View view) {
disable(view, null);
}
/**
* Disables any clicks inside the given given view for a certain amount of time.
*
* #param view The view to iterate over and disable all clicks.
* #param millis The number of millis to disable clicks for.
*/
public static void disable(View view, Long millis) {
final List<View> clickableViews = (millis == null) ? null : new ArrayList<View>();
disableRecursive(view, clickableViews);
if (millis != null) {
MainThread.handler().postDelayed(new Runnable() {
#Override public void run() {
for (View v : clickableViews) {
v.setClickable(true);
}
}
}, millis);
}
}
private static void disableRecursive(View view, List<View> clickableViews) {
if (view.isClickable()) {
view.setClickable(false);
if (clickableViews != null)
clickableViews.add(view);
}
if (view instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) view;
for (int i = 0; i < vg.getChildCount(); i++) {
disableRecursive(vg.getChildAt(i), clickableViews);
}
}
}
}
Bet my arm it's due to extra stateless fragments living somewhere inside of your app. I'd personally discourage retaining instance and let Android do what it can with it, while you use standard mechanism to keep your state (saveInstanceState, database, high-level classes/patterns, SharedPreferences, etc).
Personally I've had plenty of issues when retaining a fragment instance (normally when re-creating or re-activating fragments through config changes or leaving and re-entering the app), resulting generally on two fragments, one of them connected to views, stateless, thus useless; and the "real" one keeping the previous state without any connection to views, hence ending up with exceptions and all sort of fanciness you don't want to have.
Start by not retaining the instance and see what comes up.
You could use mHandler.removeCallbacksAndMessages(null) work in many situations for me.
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 ;)
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.
I create a layout in code in onCreate. Inside this layout, i have spinner with registred listener. When user changes item in a spinner, new data is read and layout must be changed according to data. I do this with the same function as in onCreate (i create scrollview and other views and call setContentView(scollView) on the last line).
The layout changes correctly but everything blocks, spinner and buttons can't be clicked anymore. Logcat displays no error(only lots of GC freed x objects).
I tried calling scrollview.removeAllViews() before trying to redraw layout, but that doesn't help either.
What am i missing here?
Presumably you are creating a whole new set of button and spinner objects. You need to register all the Listeners again to these new objects.
I found a problem in my onItemSelected function, where i forgot to change boolean "firstTime", so this function was called over and over.
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
if (firstTime) {
//do nothing (just change the flag) because we don't want this called during UI building
firstTime=false;
}
else {
posit=pos;
String koda=Roaming.m.get(parent.getItemAtPosition(pos).toString());
Roaming.operaterji.clear();
Roaming.parsePrices(json, koda);
getAll();
firstTime=true; //FORGET TO SET FLAG BACK TO TRUE, SO THIS WAS CALLED IN A LOOP
makeGui();
}
}