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);
Related
I have a ViewPager with a couple of RecyclerViews as pages. I would like to implement functionality where RecyclerViews which are on other pages move by certain amount after user starts scrolling pages.
#Override
public void onPageScrolled(int position, float offset, int offsetPx) {
RecyclerView view1 = getPage(position - 1);
RecyclerView view2 = getPage(position + 1);
if(scrollNeeded()) {
view1.scrollBy(0, 200);
view2.scrollBy(0, 200);
}
}
The problem which I have is that everything works fine if I scroll slowly through my ViewPager but if I scroll crazy fast, some RecyclerViews don't get scrolled. I guess I somehow need to synchronize this method.
Any idea how to solve this problem? User shouldn't see that scroll.
ViewPager keeps +1 page left and right preloaded. Which means
in very beginning - current page and the next one
at the very end - last page and the previous one
anywhere else - current, previous and next
When user swipes really fast through pages, there is a real case where the page (your RecyclerView instance and its adapter) are still preparing, so they miss the scrollBy() call.
You can solve this in different ways.
Easiest is increasing the number of cached off screen pages (e.g. 3) by calling viewPager.setOffscreenPageLimit(3) - for more ViewPager.setOffScreenPageLimit(int). If you rely on page refreshes every time user swipes, this might be an issue.
Another option is creating a custom view for your RecyclerView page and adding a scroll value to be set from outside, e.g.
// in your custom page view
private RecyclerView.Adapter adapter;
private boolean needToScroll;
public void setNeedToScroll(boolean needToScroll) {
this.needToScroll = needToScroll;
// if adapter is not null (i.e. already set), scroll as is
// and set the value to false
if (adapter != null) {
this.needToScroll = false;
scrollBy(0, 200);
}
}
// and then in the place where you define your adapter, but after setting it
if (needToScroll) {
needToScroll = false;
scrollBy(0, 200);
}
Finally your view pager scroll listener
#Override
public void onPageScrolled(int position, float offset, int offsetPx) {
if(scrollNeeded()) {
Page view1 = getPage(position - 1);
Page view2 = getPage(position + 1);
view1.needToScroll(true);
view2.needToScroll(true);
}
}
I want to find out the position or ids related to a ListView's items: only those ones which are completely visible on the screen.
Using listview.getFirstVisibleposition and listview.getLastVisibleposition takes partial list items into account.
I followed a little bit similar approach as suggested by Rich, to suit my requirement which was to fetch completely visible items on screen when List View is scrolled every time.
This is what i did
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//Loop to get tids of all completely visible List View's item scrolled on screen
for (int listItemIndex = 0; listItemIndex <= getListView().getLastVisiblePosition() - getListView().getFirstVisiblePosition(); listItemIndex++) {
View listItem = getListView().getChildAt(listItemIndex);
TextView tvNewPostLabel = (TextView) listItem.findViewById(R.id.tvNewPostLabel);
if (tvNewPostLabel != null && tvNewPostLabel.getVisibility() == View.VISIBLE) {
int listTid = (int) tvNewPostLabel.getTag();
if (listItem.getBottom() < getListView().getHeight()) {//If List View's item is not partially visible
listItemTids.add(listTid);
}
}
}
}
I have not tried this, but here are the pieces of the framework that I believe will get you to what you're looking for (at least this is what I'd try first)
As you've stated, you should get the last visible position from the list view using ListView.getLastVisiblePosition()
You can then access the View representing this position using ListView.getChildAt(position)
You now have a reference to the view, which you can call a combination of View.getLocationOnScreen(location) and View.getHeight()
Also call View.getLocationOnScreen(location) and View.getHeight() on the ListView. y + height of the View should be less than or equal to y + height of the ListView if it is fully visible.
I am trying to make the background of a ListView scroll with the ListView. I am basing my approach on this class in Shelves, but while in Shelves everything has the same height, I can't make the same guarantee.
I have an activity like so:
public class MainActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView = (ListView) findViewById(R.id.listView);
List<String> items = new ArrayList<String>();
for(int i=0; i < 100; ++i) {
items.add("Hello " + i);
}
CustomArrayAdapter adapter = new CustomArrayAdapter(this, items);
listView.setAdapter(adapter);
}
}
Where CustomArrayAdapter is:
public class CustomArrayAdapter extends ArrayAdapter<String> {
private List<Integer> mHeights;
public View getView(int position, View convertView, ViewGroup parent) {
//snip
}
}
What I want to do is populate mHeights in the adapter with heights of the rows of the view.
My main attempt was to do this, in getView():
if(row.getMeasuredHeight() == 0) {
row.measure(
MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY));
}
mHeights.set(position, row.getMeasuredHeight());
I've tried several ways to do this, but I can't get anything to work.
In getView, if I call getHeight() I get a return value of 0, while if I call getMeasuredHeight(), I get something non-zero but not the real height. If I scroll down and then up again (taking one of the rows out of view) getHeight() == getMeasuredHeight() and both have the correct value. If I just update mHeights in getView as I go (by saying mHeights.set(position, row.getHeight()); it works - but only after I've scrolled to the bottom of the ListView. I've tried calling row.measure() within the getView method, but this still causes getHeight() to be 0 the first time it is run.
My question is this: how do I calculate and populate mHeights with the correct heights of the rows of the ListView? I've seen some similar questions, but they don't appear to be what I'm looking for. The ViewTreeObserver method seems promising, but I can't figure out how to force calls to getView() from it (alternately, to iterate the rows of the ListView). Calling adapter.notifyDataSetChanged() causes an infinite (although non-blocking) loop.
Get Actual Height of View
how to get height of the item in a listview?
This is related to my previous question on the subject, which seems to have missed the point: ListView distance from top of the list
I figured out how to do this. It's was a bit tricky, but it works.
The subtlety was getting layout parameters when they are known. As I learned, getView() is returning the view for the initial time, so of course it can't have a height yet - the parent doesn't know about the row yet (since getView()'s job is to inflate it), so we need a callback.
What might that callback be? It turns out, as best I can tell it's the OnLayoutChangeListener() for the row's view. So in getView() in my custom ArrayAdapter, I added a callback for onLayoutChanged like so:
row.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
public void onLayoutChange(View v, int left, int top, int right,
int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
ref.removeOnLayoutChangeListener(this);
Log.d("CustomArrayAdapter", "onLayoutChange(" + position + "): height " + (bottom - top));
mHeights.set(position, (bottom - top));
if(position > 0) {
mDistances.set(position, bottom - top + mDistances.get(position-1) + ((ListView)parent).getDividerHeight());
}
holderRef.distanceFromTop = mDistances.get(position);
Log.d("CustomArrayAdapter", "New height for " + position + " is " + mHeights.get(position) + " Distance: " + mDistances.get(position));
}
});
Here, row is the view to be returned by getView() in the adapter, mHeights is a list of the height of each element of the ListView, and mDistances is a list of the distance from the top of the list view in pixels - the real thing that I wanted to calculate (that is, if you laid out the entire list view end to end, how far from the top element i would be from the top). All of these are stored within the custom ArrayAdapter. These lists are initialized to zeroes.
Adding this callback allowed me to calculate the height of the view.
My goal was to have a ListView that has a background that scrolls with the list. If anyone is interested, I put the code up on GitHub if anyone would like to review it: https://github.com/mdkess/TexturedListView
This SO answer should work for developers who also want to support API level < 11.
Basically, instead of adding a View.onLayoutChnageListener to your view, you can set a OnGlobalLayoutListener on your view's ViewTreeObserver.
I have a ListView, which contains more elements then I can display at one time.Now I want to get Index off all Elements, which are full visible ( -> excluding those that are only partially visible).
At this moment I use getFirstVisiblePosition() & getLastVisiblePosition() into an for-loop to iterate them, but these method is not accurate as I want to.
Is there any better solution?
A ListView keeps its rows organized in a top-down list, which you can access with getChildAt(). So what you want is quite simple. Let's get the first and last Views, then check if they are completely visible or not:
// getTop() and getBottom() are relative to the ListView,
// so if getTop() is negative, it is not fully visible
int first = 0;
if(listView.getChildAt(first).getTop() < 0)
first++;
int last = listView.getChildCount() - 1;
if(listView.getChildAt(last).getBottom() > listView.getHeight())
last--;
// Now loop through your rows
for( ; first <= last; first++) {
// Do something
View row = listView.getChildAt(first);
}
Addition
Now I want to get Index off all Elements, which are full visible
I'm not certain what that sentence means. If the code above isn't the index you wanted you can use:
int first = listView.getFirstVisiblePosition();
if(listView.getChildAt(0).getTop() < 0)
first++;
To have an index that is relative to your adapter (i.e. adapter.getItem(first).)
The way I would do this is I would extend whatever view your are passing in getView of the ListView adapter and override the methods onAttachedToWindow and onDetachedToWindow to keep track of the indexes that are visible.
Try onScrollListner and you can able to use getFirstVisiblePosition and getLastVisiblePosition.
This this link, it contain similar type of problem. I suppose you got your answer there..,.
The above code is somewhat correct. If you need to find the completely visible location of view use below code
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
View v = null;
if (scrollState == 0) {
int first =0;
if (view.getChildAt(first).getTop() < 0)
first++;
int last = list.getChildCount() - 1;
if (list.getChildAt(last).getBottom() > list
.getHeight())
last--;
// Now loop through your rows
for ( ; first <= last; first++) {
// Do something
View row = view.getChildAt(first);
// postion for your row............
int i=list.getPositionForView(row);
}
}
// set the margin.
}
I'm creating a list of pictures using a ListView and the photos are of a size that would fit 2 to 3 photos on the screen.
The problem that I'm having is that I would like to when the user stops scrolling that the first item of the visible list would snap to the top of screen, for example, if the scroll ends and small part of the first picture displayed, we scroll the list down so the picture is always fully displayed, if mostly of the picture is displayed, we scroll the list up so the next picture is fully visible.
Is there a way to achieve this in android with the listview?
I've found a way to do this just listening to scroll and change the position when the scroll ended by implementing ListView.OnScrollListener
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case OnScrollListener.SCROLL_STATE_IDLE:
if (scrolling){
// get first visible item
View itemView = view.getChildAt(0);
int top = Math.abs(itemView.getTop()); // top is a negative value
int bottom = Math.abs(itemView.getBottom());
if (top >= bottom){
((ListView)view).setSelectionFromTop(view.getFirstVisiblePosition()+1, 0);
} else {
((ListView)view).setSelectionFromTop(view.getFirstVisiblePosition(), 0);
}
}
scrolling = false;
break;
case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
case OnScrollListener.SCROLL_STATE_FLING:
Log.i("TEST", "SCROLLING");
scrolling = true;
break;
}
}
The change is not so smooth but it works.
Utilizing a couple ideas from #nininho's solution, I got my listview to snap to the item with a smooth scroll instead of abruptly going to it. One caveat is that I've only tested this solution on a Moto X in a basic ListView with text, but it works very well on the device. Nevertheless, I'm confident about this solution, and encourage you to provide feedback.
listview.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
if (scrollState == SCROLL_STATE_IDLE) {
View itemView = view.getChildAt(0);
int top = Math.abs(itemView.getTop());
int bottom = Math.abs(itemView.getBottom());
int scrollBy = top >= bottom ? bottom : -top;
if (scrollBy == 0) {
return;
}
smoothScrollDeferred(scrollBy, (ListView)view);
}
}
private void smoothScrollDeferred(final int scrollByF,
final ListView viewF) {
final Handler h = new Handler();
h.post(new Runnable() {
#Override
public void run() {
// TODO Auto-generated method stub
viewF.smoothScrollBy(scrollByF, 200);
}
});
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
// TODO Auto-generated method stub
}
});
The reason I defer the smooth scrolling is because in my testing, directly calling the smoothScrollBy method in the state changed callback had problems actually scrolling. Also, I don't foresee a fully-tested, robust solution holding very much state, and in my solution below, I hold no state at all. This solution is not yet in the Google Play Store, but should serve as a good starting point.
Using #nininho 's solution,
In the onScrollStateChanged when the state changes to SCROLL_STATE_IDLE, remember the position to snap and raise a flag:
snapTo = view.getFirstVisiblePosition();
shouldSnap = true;
Then, override the computeScroll() method:
#Override
public void computeScroll() {
super.computeScroll();
if(shouldSnap){
this.smoothScrollToPositionFromTop(snapTo, 0);
shouldSnap = false;
}
}
You can do a much more smooth scrolling if you use RecyclerView. The OnScrollListener is way better.
I have made an example here: https://github.com/plattysoft/SnappingList
Well.. I know 10 years have past since this question was asked, but now we can use LinearSnapHelper:
new LinearSnapHelper().attachToRecyclerView(recyclerView);
Source:
https://proandroiddev.com/android-recyclerview-snaphelper-19eaa9598da6
Apart from trying the code above one thing you should make sure of is that your listView have a height that can fit exact number of items you want to be displayed.
e.g
If you want 4 items to be displayed after snap effect and your row height (defined in its layout) should be 1/4 of the total height of the list.
Note that after the smoothScrollBy() call, getFirstVisiblePosition() may point to the list item ABOVE the topmost one in the listview. This is especially true when view.getChildAt(0).getBottom() == 0. I had to call view.setSelection(view.getFirstVisiblePosition() + 1) to remedy this odd behavior.