I have a ListView. I updated its adapter, and call notifydatasetchanged(). I want to wait until the list finishes drawing and then call getLastVisiblePosition() on the list to check the last item.
Calling getLastVisiblePosition() right after notifydatasetchanged() doesn't work because the list hasnt finished drawing yet.
Hopefully this can help:
Setup an addOnLayoutChangeListener on the listview
Call .notifyDataSetChanged();
This will fire off the OnLayoutChangeListener when completed
Remove the listener
Perform code on update (getLastVisiblePosition() in your case)
mListView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
mListView.removeOnLayoutChangeListener(this);
Log.e(TAG, "updated");
}
});
mAdapter.notifyDataSetChanged();
I think this implementation can solve the problem.
// draw ListView in UI thread
mListAdapter.notifyDataSetChanged();
// enqueue a message to UI thread
mListView.post(new Runnable() {
#Override
public void run() {
// this will be called after drawing completed
}
});
You can implement callback, after the notifyDataSetChanged() it will be called and do the job you need. It means the data has been loaded to the adapter and at next draw cycle will be visible on the GUI. This mechanism is built-in in Android. See DataSetObserver.
Implement his callback and register it into the list adapter, don't forget to unregister it, whenevr you no longer need it.
/**
* #see android.widget.ListAdapter#setAdapter(ListAdapter)
*/
#Override
public void setAdapter(android.widget.ListAdapter adapter) {
super.setAdapter(adapter);
adapter.registerDataSetObserver(new DataSetObserver() {
#Override
public void onChanged() {
onListChanged();
// ... or do something else here...
super.onChanged();
}
});
}
Related
I have a ListView with a set of children vertically listed (View objects) to be viewed by the users. I have to track the the user views, say,
a. If a user views a set of items for around 1 second, I should track the impressions.
b. If the same user scrolls the items out of the viewport and return back, I should track again, if he viewed for 1 second.
I tried several options like getGlobalVisibleRect(), getLocalVisibleRect(), getLocationOnScreen() and they are confusing in the first place and didn't help me get the right coordinates and visibility of the child items of the listView.
I checked Track impression of items in an android ListView which is a bit similar to my requirement but I thought to check if there is a better solution. I am new to Android and apologies if I am not clear on some explanations
To get your desired result, I think we have two different solutions. First, create Handler for each of the item and call / remove in scroll view if it is visible. But this is very much stupid one as creating so many Handlers will make your app's life hell.
Second and best way is to use call / remove a single Handler for the entire visible items. If it persist for a time "A second" (1 second for you), use impression count in each of your item's model class and increase it with ++ operator.
You can add scroll listener in your listivew. The script will be like-
ListView listView = null;
int firstVisibleItemIndex = 0;
int visibleCount = 0;
Handler handler = new Handler();
Runnable runnable = new Runnable() {
#Override
public void run() {
for (int i = firstVisibleItemIndex; i < firstVisibleItemIndex + visibleCount; i++) {
try {
//Get impression count from model for the visible item index i
int count = modelList.get(i).getImpressionCount();
//Set impression count to the model for the visible item index i
modelList.get(i).setImpressionCount(++count);
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
//Can call this method body in onCreate directly
private void addListScrollListener() {
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// You cat determine first and last visible items here
// final int lastVisibleItem = firstVisibleItem + visibleItemCount - 1;
handler.removeCallbacks(runnable);
firstVisibleItemIndex = firstVisibleItem;
visibleCount = visibleItemCount;
handler.postDelayed(runnable, 1000);
}
#Override
public void onScrollStateChanged(AbsListView arg0, int arg1) {
// TODO Auto-generated method stub
}
});
}
I assume that you will bind your ListView with the id in your onCreate method. Also you can call the listener thing in your onCreate after binding the view with the variable.
I hope this will work for your requirement.
Let me know your feedback.
So I have a RecyclerView that has multiple view types that all have a different rendering background-wise. Naturally I want to avoid overdraw for all these components, so I give my RecyclerView and all views up in the hierarchy no background at all.
This works fine as is - until I start animating items in and out. The DefaultItemAnimator of course nicely blends items in and out and therefor opens a "hole" in the RecyclerView where the background of it shortly becomes visible.
Ok, I thought, lets try something - let's give the RecyclerView only a background when animations are actually running, but otherwise remove the background, so scrolling works smoothly at high FPS rates. However, this is actually harder than I originally thought, since there is no specific "animations will start" and corresponding "animations will end" signal in RecyclerView nor the ItemAnimator or related classes.
What I recently tried was to combine an AdapterDataObserver with an ItemAnimatorFinishedListener like this, but without success:
RecyclerView.ItemAnimator.ItemAnimatorFinishedListener finishListener =
new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
#Override
public void onAnimationsFinished() {
recycler.setBackgroundResource(0);
}
};
recycler.getAdapter().registerAdapterDataObserver(
new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
start();
}
#Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
start();
}
#Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
start();
}
private void start() {
recycler.setBackgroundResource(R.color.white);
if (!recycler.getItemAnimator().isRunning()) {
return;
}
recycler.getItemAnimator().isRunning(finishListener);
}
}
);
The issue here is that the adapter's range callbacks are ran way earlier than the actual animations run, because the animations will not be scheduled before the next requestLayout() happens internally in the RecyclerView, i.e. recycler.getItemAnimator().isRunning() in my start() method always returns false, so the white background is never removed.
So before I start experimenting with an additional ViewTreeObserver.OnGlobalLayoutListener and bring that into the mix - has anybody found a proper, working (easier?!) solution to this problem?
Ok, I went further down the road and included a ViewTreeObserver.OnGlobalLayoutListener - this seems to be working:
/**
* This is a utility class that monitors a {#link RecyclerView} for changes and temporarily
* gives the view a background so we do not see any artifacts while items are animated in or
* out of the view, and, at the same time prevent the overdraw that would occur when we'd
* give the {#link RecyclerView} a permanent opaque background color.
* <p>
* Created by Thomas Keller <me#thomaskeller.biz> on 12.05.16.
*/
public class RecyclerBackgroundSaver {
private RecyclerView mRecyclerView;
#ColorRes
private int mBackgroundColor;
private boolean mAdapterChanged = false;
private ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener
= new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// ignore layout changes until something actually changed in the adapter
if (!mAdapterChanged) {
return;
}
mRecyclerView.setBackgroundResource(mBackgroundColor);
// if no animation is running (which should actually only be the case if
// we change the adapter without animating anything, like complete dataset changes),
// do not do anything either
if (!mRecyclerView.getItemAnimator().isRunning()) {
return;
}
// remove this view tree observer, i.e. do not react on further layout changes for
// one and the same dataset change and give control to the ItemAnimatorFinishedListener
mRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
mRecyclerView.getItemAnimator().isRunning(finishListener);
}
};
RecyclerView.ItemAnimator.ItemAnimatorFinishedListener finishListener
= new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
#Override
public void onAnimationsFinished() {
// the animation ended, reset the adapter changed flag so the next change kicks off
// the cycle again and add the layout change listener back
mRecyclerView.setBackgroundResource(0);
mAdapterChanged = false;
mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
}
};
RecyclerView.AdapterDataObserver mAdapterDataObserver = new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
mAdapterChanged = true;
}
#Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
mAdapterChanged = true;
}
#Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
mAdapterChanged = true;
}
};
public RecyclerBackgroundSaver(RecyclerView recyclerView, #ColorRes int backgroundColor) {
mRecyclerView = recyclerView;
mBackgroundColor = backgroundColor;
}
/**
* Enables the background saver, i.e for the next item change, the RecyclerView's background
* will be temporarily set to the configured background color.
*/
public void enable() {
checkNotNull(mRecyclerView.getAdapter(), "RecyclerView has no adapter set, yet");
mRecyclerView.getAdapter().registerAdapterDataObserver(mAdapterDataObserver);
mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
}
/**
* Disables the background saver, i.e. for the next animation,
* the RecyclerView's parent background will again shine through.
*/
public void disable() {
mRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener);
if (mRecyclerView.getAdapter() != null) {
mRecyclerView.getAdapter().unregisterAdapterDataObserver(mAdapterDataObserver);
}
}
}
From my understanding, there's no API for developers to determine when AdapterView's are getting redrawn.
We call notifyDataSetChanged() and then, at some point in the future, with no event for us to listen for, the ListView redraws it's views.
I say this because I've encountered a situation where I am updating images in a ListView when the scroll has stopped.
Every time I set a new list source - i.e. call notifyDataSetChanged() from my adapter, I then call my updateImagesInView() method - kind of like this:
//MyListView.java
public void setDataSource(SomeClass dataSource) {
((MyListAdapter)myListView.getAdapter()).setSomeDataSource(dataSource);
updateImagesInView();
}
public void updateImagesInView() {
for (int i = 0; i <= mListView.getLastVisiblePosition() - mListView.getFirstVisiblePosition(); i++) {
View listItemView = mListView.getChildAt(i);
...
}
}
//MyListAdapater.java
public void setSomeDataSource(SomeClass dataSource) {
mDataSource = dataSource;
notifyDataSetChanged();
}
The child views I get from the loop in the updateImagesInView method always belong to the previous dataSource.
I've hacked in a workaround, so I'm not looking for a "how to do this" answer, but more along the lines of - is there anyway to know when the views in a ListView have actually been updated after calling notifyDataSetChanged()? (or am I just doing something crazy wrong because the views should effectively be updated immediately after calling notifyDataSetChanged()?)
Well you can add a listener to yourListView's layout like:
mListView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
mListView.removeOnLayoutChangeListener(this);
Log.e(TAG, "updated");
}
});
mAdapter.notifyDataSetChanged();
Otherwise you should listen on your adapter, as when notifyDataSetChanged is called, your adapter gets calls to getView() to update all the views that are currently visible.
I'm trying to smoothly scroll to last element of a list after adding an element to the arrayadapter associated with the listview.
The problem is that it just scrolls to a random position
arrayadapter.add(item);
//DOES NOT WORK CORRECTLY:
listview.smoothScrollToPosition(arrayadapter.getCount()-1);
//WORKS JUST FINE:
listview.setSelection(arrayadapter.getCount()-1);
You probably want to tell the ListView to post the scroll when the UI thread can handle it (which is why yours it not scrolling properly). SmoothScroll needs to do a lot of work, as opposed to just go to a position ignoring velocity/time/etc. (required for an "animation").
Therefore you should do something like:
getListView().post(new Runnable() {
#Override
public void run() {
getListView().smoothScrollToPosition(pos);
}
});
(Copied from my answer: smoothScrollToPositionFromTop() is not always working like it should)
This is a known bug. See https://code.google.com/p/android/issues/detail?id=36062
However, I implemented this workaround that deals with all edge cases that might occur:
First call smothScrollToPositionFromTop(position) and then, when scrolling has finished, call setSelection(position). The latter call corrects the incomplete scrolling by jumping directly to the desired position. Doing so the user still has the impression that it is being animation-scrolled to this position.
I implemented this workaround within two helper methods:
smoothScrollToPosition()
public static void smoothScrollToPosition(final AbsListView view, final int position) {
View child = getChildAtPosition(view, position);
// There's no need to scroll if child is already at top or view is already scrolled to its end
if ((child != null) && ((child.getTop() == 0) || ((child.getTop() > 0) && !view.canScrollVertically(1)))) {
return;
}
view.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(final AbsListView view, final int scrollState) {
if (scrollState == SCROLL_STATE_IDLE) {
view.setOnScrollListener(null);
// Fix for scrolling bug
new Handler().post(new Runnable() {
#Override
public void run() {
view.setSelection(position);
}
});
}
}
#Override
public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount,
final int totalItemCount) { }
});
// Perform scrolling to position
new Handler().post(new Runnable() {
#Override
public void run() {
view.smoothScrollToPositionFromTop(position, 0);
}
});
}
getChildAtPosition()
public static View getChildAtPosition(final AdapterView view, final int position) {
final int index = position - view.getFirstVisiblePosition();
if ((index >= 0) && (index < view.getChildCount())) {
return view.getChildAt(index);
} else {
return null;
}
}
You should use setSelection() method.
Use LayoutManager to smooth scroll
layoutManager.smoothScrollToPosition(recyclerView, new RecyclerView.State(), position);
final Handler handler = new Handler();
//100ms wait to scroll to item after applying changes
handler.postDelayed(new Runnable() {
#Override
public void run() {
listView.smoothScrollToPosition(selectedPosition);
}}, 100);
The set selection method mentioned by Lars works, but the animation was too jumpy for our purposes as it skips whatever was left. Another solution is to recall the method repeatedly until the first visible position is your index. This is best done quickly and with a limit as it will fight the user scrolling the view otherwise.
private void DeterminedScrollTo(Android.Widget.ListView listView, int index, int attempts = 0) {
if (listView.FirstVisiblePosition != index && attempts < 10) {
attempts++;
listView.SmoothScrollToPositionFromTop (index, 1, 100);
listView.PostDelayed (() => {
DeterminedScrollTo (listView, index, attempts);
}, 100);
}
}
Solution is in C# via. Xamarin but should translate easily to Java.
Do you call arrayadapter.notifyDataSetChanged() after you called arrayadapter.add()? Also to be sure, smoothScrollToPosition and setSelection are methods available in ListView not arrayadapter as you have mentioned above.
In any case see if this helps:
smoothScrollToPosition after notifyDataSetChanged not working in android
Solution in kotlin:
fun AbsListView.scrollToIndex(index: Int, duration: Int = 150) {
smoothScrollToPositionFromTop(index, 0, duration)
postDelayed({
setSelection(index)
post { smoothScrollToPositionFromTop(index, 0, duration) }
}, duration.toLong())
}
PS: Looks like its quite messed up on Android SDK side so this is kind of best you can get, if you don't want to calculate your view offset manually. Maybe best easy way is to set duration to 0 for long list to avoid any visible jump.
I had some issues when calling just setSelection in some positions in GridView so this really seems to me as solution instead of using that.
use this in android java, it work for me:
private void DeterminedScrollTo(int index, int attempts) {
if (listView.getFirstVisiblePosition() != index && attempts < 10) {
attempts++;
if (listView.canScrollVertically(pos))
listView.smoothScrollToPositionFromTop(index, 1, 200);
int finalAttempts = attempts;
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
#Override
public void run() {
DeterminedScrollTo(index, finalAttempts);
}
}, 200);
}
}
This is my first ever post here and I'm a dumb novice, so I hope someone out there can both help me and excuse my ignorance.
I have a ListView which is populated with an ArrayAdapter. When I either scroll or click, I want the selected item, or the item nearest the vertical center, to be forced to the exact vertical center of the screen. If I call listView.setSelection(int position) it aligns the selected position at the top of the screen, so I need to use listView.setSelectionFromTop(position, offset) instead. To find my offset, I take half of the View's height from the half of the ListView's height.
So, I can vertically center my item easy enough, within OnItemClick or OnScrollStateChanged, with the following:
int x = listView.getHeight();
int y = listView.getChildAt(0).getHeight();
listView.setSelectionFromTop(myPosition, x/2 - y/2);
All this works fine. My problem is with the initial ListView setup. I want an item to be centered when the activity starts, but I can't because I get a NullPointerException from:
int y = listView.getChildAt(0).getHeight();
I understand this is because the ListView has not yet rendered, so it has no children, when I call this from OnCreate() or OnResume().
So my question is simply: how can I force my ListView to render at startup so I can get the height value I need? Or, alternatively, is there any other way to center items vertically within a ListView?
Thanks in advance for any help!
int y = listView.getChildAt(0).getHeight();
I understand this is because the ListView has not yet rendered, so it has no children, when I call this from onCreate() or onResume().
You should call it in onScroll.
listView.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
//Write your logic here
int y = listView.getChildAt(0).getHeight();
}
});
I'm answering my own question here, but it's very much a hack. I think it's interesting because it sheds some light on the behavior of listviews.
The problem was in trying to act on data (a listview row) that did not yet exist (it had not been rendered). listview.getChildAt(int) was null because the listview had no children yet. I found out onScroll() is called immediately when the activity is created, so I simply put everything in a thread and delayed the getChildAt() call. I then enclosed the whole thing in a boolean wrapper to make sure it is only ever called once (on startup).
The interesting thing was that I only had to delay the call by 1ms for everything to be OK. And that's too fast for the eye to see.
Like I said, this is all a hack so I'm sure all this is a bad idea. Thanks for any help!
private boolean listViewReady = false;
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (!listViewReady){
Thread timer = new Thread() {
public void run() {
try{
sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}finally{
runOnUiThread(new Runnable() {
public void run() {
myPosition = 2;
int x = listView.getHeight();
int y = listView.getChildAt(0).getHeight();
listView.setSelectionFromTop(myPosition, x/2 - y/2);
listViewReady = true;
}
});
}
}
};
timer.start();
}//if !ListViewReady
I have achieved the same using a in my opinion slighlty simpler solution
mListView.post(new Runnable() {
#Override
public void run() {
int height = mListView.getHeight();
int itemHeight = mListView.getChildAt(0).getHeight();
if (positionOfMyItem == myCollection.size() - 1) {
// last element - > don't subtract item height
itemHeight = 0;
}
mListView.setSelectionFromTop(position, height / 2 - itemHeight / 2);
}
});