Understanding When ListView Views actually get redrawn after calling notifyDataSetChanged() - android

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.

Related

Track user impressions of view items in Android ListView

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.

RecyclerView - Scroll To Position Not Working Every Time

I have implemented a horizontal scrollable RecyclerView. My RecyclerView uses a LinearLayoutManager, and the problem I am facing is that when I try to use scrollToPosition(position) or smoothScrollToPosition(position) or from LinearLayoutManager's scrollToPositionWithOffset(position). Neither works for me. Either a scroll call doesn't scroll to the desired location or it doesn't invoke the OnScrollListener.
So far I have tried so many different combinations of code that I cannot post them all here. Following is the one that works for me (But only partially):
public void smoothUserScrollTo(final int position) {
if (position < 0 || position > getAdapter().getItemCount()) {
Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
return;
}
if (getLayoutManager() == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
"Call setLayoutManager with a non-null layout.");
return;
}
if (getChildAdapterPosition(getCenterView()) == position) {
return;
}
stopScroll();
scrollToPosition(position);
if (lastScrollPosition == position) {
addOnLayoutChangeListener(new OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (left == oldLeft && right == oldRight && top == oldTop && bottom == oldBottom) {
removeOnLayoutChangeListener(this);
updateViews();
// removing the following line causes a position - 3 effect.
scrollToView(getChildAt(0));
}
}
});
}
lastScrollPosition = position;
}
#Override
public void scrollToPosition(int position) {
if (position < 0 || position > getAdapter().getItemCount()) {
Log.e(TAG, "An attempt to scroll out of adapter size has been stopped.");
return;
}
if (getLayoutManager() == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager is not set. " +
"Call setLayoutManager with a non-null layout.");
return;
}
// stopScroll();
((LinearLayoutManager) getLayoutManager()).scrollToPositionWithOffset(position, 0);
// getLayoutManager().scrollToPosition(position);
}
I opted for scrollToPositionWithOffset() because of this but the case perhaps is different as I use a LinearLayoutManager instead of GridLayoutManager. But the solution does work for me too, but as I said earlier only partially.
When the call to scroll is from 0th position to totalSize - 7 scroll works like a charm.
When scroll is from totalSize - 7 to totalSize - 3, First time I only scroll to 7th last item in the list. The second time however I can scroll fine
When scrolling from totalSize - 3 to totalSize, I start getting unexpected behavior.
If anyone has found a work around I'd Appreciate it. Here's the gist to my code of custom ReyclerView.
I had the same issue some weeks ago, and found only a really bad solution to solve it. Had to use a postDelayed with 200-300ms.
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
yourList.scrollToPosition(position);
}
}, 200);
If you found a better solution, please let me know! Good luck!
Turns out I was having a similar issue until I utilized
myRecyclerview.scrollToPosition(objectlist.size()-1)
It would always stay at the top when only putting in the objectlist size. This was until i decided to set the size equal to a variable. Again, that didn't work. Then I assumed that perhaps it was handling an outofboundsexception without telling me. So I subtracted it by 1. Then it worked.
The accepted answer will work, but it may also break. The main reason for this issue is that the recycler view may not be ready by the time you ask it to scroll. The best solution for the same is to wait for the recycler view to be ready and then scroll. Luckily android has provided one such option. Below solution is for Kotlin, you can try the java alternative for the same, it will work.
newsRecyclerView.post {
layoutManager?.scrollToPosition(viewModel.selectedItemPosition)
}
The post runnable method is available for every View elements and will execute once the view is ready, hence ensuring the code is executed exactly when required.
You can use LinearSmoothScroller this worked every time in my case:
First create an instance of LinearSmoothScroller:
LinearSmoothScroller smoothScroller=new LinearSmoothScroller(activity){
#Override
protected int getVerticalSnapPreference() {
return LinearSmoothScroller.SNAP_TO_START;
}
};
And then when you want to scroll recycler view to any position do this:
smoothScroller.setTargetPosition(pos); // pos on which item you want to scroll recycler view
recyclerView.getLayoutManager().startSmoothScroll(smoothScroller);
Done.
So the problem for me was that I had a RecyclerView in a NestedScrollView. Took me some time to figure out this was the problem. The solution for this is (Kotlin):
val childY = recycler_view.y + recycler_view.getChildAt(position).y
nested_scrollview.smoothScrollTo(0, childY.toInt())
Java (credits to Himagi https://stackoverflow.com/a/50367883/2917564)
float y = recyclerView.getY() + recyclerView.getChildAt(selectedPosition).getY();
scrollView.smoothScrollTo(0, (int) y);
The trick is to scroll the nested scrollview to the Y instead of the RecyclerView. This works decently at Android 5.0 Samsung J5 and Huawei P30 pro with Android 9.
I also faced a similar problem (having to scroll to the top when the list is getting updated), but none of the above options worked 100%
However I finally found a working solution at https://dev.to/aldok/how-to-scroll-recyclerview-to-a-certain-position-5ck4 archive link
Summary
scrollToPosition only seems to work when the underlying dataset is ready.
So therefore postDelay works (randomly) but it's depending on the speed of the device/app. If the timeout is too short it fails. smoothScrollToPosition also only works if the adapter is not too busy (see https://stackoverflow.com/a/61403576/11649486)
To observe when the dataset is ready, a AdapterDataObserver can be added and certain methods overridden.
The code that fixed my problem:
adapter.registerAdapterDataObserver( object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(
positionStart: Int,
itemCount: Int
) {
// This will scroll to the top when new data was inserted
recyclerView.scrollToPosition(0)
}
}
None of the methods seems to be working for me. Only the below single line of code worked
((LinearLayoutManager)mRecyclerView.getLayoutManager()).scrollToPositionWithOffset(adapter.currentPosition(),200);
The second parameter refers to offset, which is actually the distance (in pixels) between the start edge of the item view and start edge of the RecyclerView. I have supplied it with a constant value to make the top items also visible.
Check for more reference over here
Using Kotlin Coroutines in Fragment or Activity, and also using the lifecycleScope since any coroutine launched in this scope is canceled when the Lifecycle is destroyed.
lifecycleScope.launch {
delay(100)
recyclerView.scrollToPosition(0)
This worked for me
Handler().postDelayed({
(recyclerView.getLayoutManager() as LinearLayoutManager).scrollToPositionWithOffset( 0, 0)
}, 100)
I had the same issue while creating a cyclic/circular adapter, where I could only scroll downward but not upward considering the position initialises to 0. I first considered using Robert's approach, but it was too unreliable as the Handler only fired once, and if I was unlucky the position wouldn't get initialised in some cases.
To resolve this, I create an interval Observable that checks every XXX amount of time to see whether the initialisation succeeded and afterward disposes of it. This approach worked very reliably for my use case.
private fun initialisePositionToAllowBidirectionalScrolling(layoutManager: LinearLayoutManager, realItemCount: Int) {
val compositeDisposable = CompositeDisposable() // Added here for clarity, make this into a private global variable and clear in onDetach()/onPause() in case auto-disposal wouldn't ever occur here
val initPosition = realItemCount * 1000
Observable.interval(INIT_DELAY_MS, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe ({
if (layoutManager.findFirstVisibleItemPosition() == 0) {
layoutManager.scrollToPositionWithOffset(initPosition, 0)
if (layoutManager.findFirstCompletelyVisibleItemPosition() == initPosition) {
Timber.d("Adapter initialised, setting position to $initPosition and disposing interval subscription!")
compositeDisposable.clear()
}
}
}, {
Timber.e("Failed to initialise position!\n$it")
compositeDisposable.clear()
}).let { compositeDisposable.add(it) }
}
This worked perfectly for when scrolling to last item in the recycler
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
if (((LinearLayoutManager) recyclerView.getLayoutManager())
.findLastVisibleItemPosition() != adapter.getItemCount() - 1) {
recyclerView.scrollToPosition(adapter.getItemCount() - 1);
handler.postDelayed(this, 200);
}
}
}, 200 /* change it if you want*/);
Pretty weird bug, anyway I managed to work around it without post or post delayed as follow:
list.scrollToPosition(position - 1)
list.smoothScrollBy(1, 0)
Hopefully, it helps someone too.
Had the same issue. My problem was, that I refilled the view with data in an async task, after I tried to scroll. From onPostExecute ofc fixed this problem. A Delay fixed this issue too, because when the scroll executed, the list had already been refilled.
I use below solution to make the selected item in recycler view visible after the recycler view is reloaded (orientation change, etc). It overrides LinearLayoutManager and uses onSaveInstanceState to save current recycler position. Then in onRestoreInstanceState the saved position is restored. Finaly, in onLayoutCompleted, scrollToPosition(mRecyclerPosition) is used to make the previously selected recycler position visible again, but as Robert Banyai stated, for it to work reliably a certain delay must be inserted. I guess it is needed to provide enough time for adapter to load the data before scrollToPosition is called.
private class MyLayoutManager extends LinearLayoutManager{
private boolean isRestored;
public MyLayoutManager(Context context) {
super(context);
}
public MyLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public MyLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
if(isRestored && mRecyclerPosition >-1) {
Handler handler=new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
MyLayoutManager.this.scrollToPosition(mRecyclerPosition);
}
},200);
}
isRestored=false;
}
#Override
public Parcelable onSaveInstanceState() {
Parcelable savedInstanceState = super.onSaveInstanceState();
Bundle bundle=new Bundle();
bundle.putParcelable("saved_state",savedInstanceState);
bundle.putInt("position", mRecyclerPosition);
return bundle;
}
#Override
public void onRestoreInstanceState(Parcelable state) {
Parcelable savedState = ((Bundle)state).getParcelable("saved_state");
mRecyclerPosition = ((Bundle)state).getInt("position",-1);
isRestored=true;
super.onRestoreInstanceState(savedState);
}
}
If you use recyclerview in nestedScrollView you must scroll nestScrollview
nestedScrollview.smoothScrollTo(0,0)
Maybe It's not so elegant way to do it, But this always works for me. Add a new method to the RecyclerView and use it insted of scrollToPosition:
public void myScrollTo(int pos){
stopScroll();
((LinearLayoutManager)getLayoutManager()).scrollToPositionWithOffset(pos,0);
}
The answer is to use the Post Method, it will guarantee correct execution for any action
This is the ultimate solution using kotlin in this date ... if you navigate to another fragment and go back and your recyclerview resets to the first position just add this line in onCreateView or wherever you need can call the adapter...
pagingAdapter.stateRestorationPolicy=RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
BTW pagingAdapter is my adapter with diffUtil.
I had a similar issue, (but not the same), I try to explain it, maybe be could help someone else:
By the time I call to 'scrollToPosition' dataset is already set but some content like images loaded async (using Glide library) and probably when RecyclerView tries to compute the height amount to scroll down, image should return 0 as no loaded yet. So that gives an inaccurate scroll down I could solve it that way:
fun LinearLayoutManager.accurateScrollToPosition(position: Int) {
this.scrollToPosition(position)
this.postOnAnimation {
val realPosition = this.findFirstVisibleItemPosition()
if (position != realPosition) {
this.accurateScrollToPosition(position)
} else {
this.scrollToPosition(position) // this looks redunadant or inecessary but must be call to ensure accurate scroll
}
}
}
PD: In my case was not possible to know the size of the image to be loaded, if you know or you can resize the image you can add a placeholder on glide with de image size or override de size so recyclerView can compute the size correctly and don't need the above walkaraound.

How do I check when my ListView has finished redrawing?

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();
}
});
}

Efficient way to invalidate a child view when its parent ListView is scrolled?

I have a ListView with a custom child view as its row layout. My requirement is to invalidate the row's child view only when the ListView is scrolled. Right now I achieve this by calling ChildView.invalidate() from the child view's onDraw() method and things works very well as I expected, but this approach also invalidates the child view even when the ListView is not scrolled, so I observe that it consumes a lot of CPU when the app is running. I am looking for an inexpensive solution for this.
Is there any call back occured when the ListView is scrolled? I could not see anything.
Please answer, Thank you.
protected void onDraw (Canvas canvas) {
drawBackground(canvas);
drawContent(canvas);
this.invalidate();//Recursive calling...? But no error or warnings issued.
//super.onDraw(canvas);
}
#pskink Thanks It works! but did not solve the issue of "do it with an inexpensive approach". Please refer the comments in the code and answer what to do with arguments of onScroll() to only invalidate views which are visible on the screen. View.getChildAt(int).invalidate does not work
import android.app.ListActivity;
import android.widget.ListView;
import android.widget.AbsListView;
public class MyListActivity extends ListActivity implements ListView.OnScrollListener {
...
#Override
public void onCreate(Bundle savedInstance) {
super.onCreate(savedInstance);
setContentView(R.layout.list_activity_layout);
getListView().setOnScrollListener(this);
}
....
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
Log.e("MyListActivity", "onScrollStateChanged()");
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
Log.e("MyListActivity", "onScroll()");
Log.e("view.getId():", "" + view.getId());
//view.invalidateViews();//Calling this method does the job, but it seems that It might re-measure
// and redraw each and every views of each and every rows in the ListView..!
// So, still it is an expensive approach. is it?, but better than doing
// recursive calling of invalidate(); am I right?
// I am not satisfied with this approach.
// At least it should be possible to invalidate the views which are visible on the screen, as we expect from -
// the arguments of this method.
// Let me attempt to do it, shown below..
if(visibleItemCount != 0){
int lastVisibleItemCount = firstVisibleItem + visibleItemCount;
for(int i=firstVisibleItem; i<lastVisibleItemCount; i++){
if(view.getChildAt(i) != null){
view.getChildAt(i).invalidate();// Not works.
}
Log.e("view.getItemIdAtPosition(i):", "" + view.getItemIdAtPosition(i));
}
}
}
}
#pskink Here is the exact solution I was looking for. It only invalidates a single child view, which is in visibility range. It works without any issue.
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
//view.invalidateViews();//It does.., but still being expensive...!
//The following only invalidates a single child view of the Row view in the visibility range.
//Initializing i=0 may seem unnecessary, but it resolves many unexpected behavior of
//the ListView when it is initialized to i=firstVisibleItem.
if(visibleItemCount != 0){
int lastVisibleItemCount = firstVisibleItem + visibleItemCount;;
for(int i=0; i<lastVisibleItemCount; i++){
mRowView = mListView.getChildAt(i);//returns the row view.
if(mRowView != null){
mChildView = (CustomChildView) mRowView.findViewById(R.id.custom_child_view);//Single child of the Row View.
if(mChildView != null){
mChildView.invalidate();//only invalidates the required Child view of the row view.
}
}
Log.e("Row view pos: ", "" + i);
}
}
}

Android: ListView.getScrollY() - does it work?

I am using it, but it always returns 0, even though I have scrolled till the end of the list.
getScrollY() is actually a method on View, not ListView. It is referring to the scroll amount of the entire view, so it will almost always be 0.
If you want to know how far the ListView's contents are scrolled, you can use listView.getFirstVisiblePosition();
It does work, it returns the top part of the scrolled portion of the view in pixels from the top of the visible view. See the getScrollY() documentation. Basically if your list is taking up the full view then you will always get 0, because the top of the scrolled portion of the list is always at the top of the screen.
What you want to do to see if you are at the end of a list is something like this:
public void onCreate(final Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.main);
// The list defined as field elswhere
this.view = (ListView) findViewById(R.id.searchResults);
this.view.setOnScrollListener(new OnScrollListener() {
private int priorFirst = -1;
#Override
public void onScroll(final AbsListView view, final int first, final int visible, final int total) {
// detect if last item is visible
if (visible < total && (first + visible == total)) {
// see if we have more results
if (first != priorFirst) {
priorFirst = first;
//Do stuff here, last item is displayed, end of list reached
}
}
}
});
}
The reason for the priorFirst counter is that sometimes scroll events can be generated multiple times, so you only need to react to the first time the end of the list is reached.
If you are trying to do an auto-growing list, I'd suggest this tutorial.
You need two things to precisely define the scroll position of a listView:
To get current position:
int firstVisiblePosition = listView.getFirstVisiblePosition();
int topEdge=listView.getChildAt(0).getTop(); //This gives how much the top view has been scrolled.
To set the position:
listView.setSelectionFromTop(firstVisiblePosition,0);
// Note the '-' sign for scrollTo..
listView.scrollTo(0,-topEdge);

Categories

Resources