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.
Related
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.
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 want to be able to take a ListView and have a specific row be scrollable to the top of that Listview's bounds, even if the row is near the end and normally wouldn't be able to scroll that high in a normal android ListView (similar to how twitter works when you drill into a specific tweet and that tweet is always scrollable to the top even when there's nothing underneath it.)
Is there any way I can accomplish this task easily? I've tried measuring the row i want to scroll to the top and applying bottom padding to account for the extra space it would need, but that yields odd results (i presume because changing padding and such during the measure pass of a view is ill advised). Doing so before the measure pass doesn't work since the measured height of the cell in question (and any cells after it) hasn't happened yet.
Looks like you the setSelectionFromTop method of listview.
mListView.setSelectionFromTop(listItemIndex, 0);
I figured it out; its a bit complex but it seems to work mostly:
public int usedHeightForAndAfterDesiredRow() {
int totalHeight = 0;
for (int index = 0; index < rowHeights.size(); index++) {
int height = rowHeights.get(rowHeights.keyAt(index));
totalHeight += height;
}
return totalHeight;
}
#Override
public View getView(int position, View convertView, final ViewGroup parent) {
View view = super.getView(position, convertView, parent);
if (measuringLayout.getLayoutParams() == null) {
measuringLayout.setLayoutParams(new AbsListView.LayoutParams(parent.getWidth(), parent.getHeight()));
}
// measure the row ahead of time so that we know how much space will need to be added at the end
if (position >= mainRowPosition && position < getCount()-1 && rowHeights.indexOfKey(position) < 0) {
measuringLayout.addView(view, new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
measuringLayout.measure(MeasureSpec.makeMeasureSpec(parent.getWidth(), MeasureSpec.EXACTLY), MeasureSpec.UNSPECIFIED);
rowHeights.put(position, view.getMeasuredHeight());
measuringLayout.removeAllViews();
view.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
}
if (position == getCount()-1 && view.getLayoutParams().height == 0) {
// we know how much height the prior rows take, so calculate the last row with that.
int height = usedHeightForAndAfterDesiredRow();
height = Math.max(0, parent.getHeight() - height);
view.getLayoutParams().height = height;
}
return view;
}
This is in my adapter. It's a subclass of a merge adapter, but you can just put it in your code and substitute the super call with however you generate your rows.
the first if statement in getView() sets the layout params of a frame layout member var that is only intended for measuring, it has no parent view.
the second if statement calculates all the row heights for rows including and after the position of the row that I care about scrolling to the top. rowHeights is a SparseIntArray.
the last if statement assumes that there is one extra view with layout params already set at the bottom of the list of views whose sole intention is to be transparent and expand at will. the usedHeightForAndAfterDesiredRow call adds up all the precalculated heights which is subtracted from the parent view's height (with a min of 0 so we don't get negative heights). this ends up creating a view on the bottom that expands at will based on the heights of the other items, so a specific row can always scroll to the top of the list regardless of where it is in the list.
I have a custom ListView class (subclassed from ListView) and I need it to add a small padding to the bottom of the final view element so that it is not overlapped by a small bar that I have across the bottom of the screen. I only want to do this when the child views grow past the visible region of the listview. I am trying to use this code to achieve this:
#Override
protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// If there are hidden listview items we need to add a small padding to the last one so that it
// partially hidden by the bottom sliding drawer handle
if (this.getLastVisiblePosition() - this.getFirstVisiblePosition() + 1 > this.getCount()) {
LinearLayout v2 = (LinearLayout) this.getChildAt(this.getCount() - 1);
v2.setPadding(v2.getPaddingLeft(), v2.getPaddingTop(), v2.getPaddingRight(),
v2.getPaddingBottom() + 5);
}
}
However, the values returned by getLastVisiblePostion(), getFirstVisiblePostion() and getCount() do not reflect what the adapter holds yet. I am assuming this is because the Adapter has not yet notified the ListView of the data, but I cannot figure out where the ListView would actually know about the data and thus have the correct values. This code is being run when the Activity is being loaded.
At what point in the rendering process will I have access to this data? I should also say that I use an AsyncTask to load the data from a database and then create the Adapter in there and add it to the list view. Is there an event I can use within the ListView that will fire when the adapter adds data/causes the listview to render the new items?
I'm not sure if it's the best solution, but I know that in one of the branches of a pull-to-refresh implementation for Android, the height of the list is compared against the total height of all the list items. From what you're saying, it sounds like that's the same information you're looking for in order to determine whether to apply extra padding or not.
The relevant parts of the implementation are in the following three methods:
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mHeight == -1) { // do it only once
mHeight = getHeight(); // getHeight only returns useful data after first onDraw()
adaptFooterHeight();
}
}
/**
* Adapts the height of the footer view.
*/
private void adaptFooterHeight() {
int itemHeight = getTotalItemHeight();
int footerAndHeaderSize = mFooterView.getHeight()
+ (mRefreshViewHeight - mRefreshOriginalTopPadding);
int actualItemsSize = itemHeight - footerAndHeaderSize;
if (mHeight < actualItemsSize) {
mFooterView.setHeight(0);
} else {
int h = mHeight - actualItemsSize;
mFooterView.setHeight(h);
setSelection(1);
}
}
/**
* Calculates the combined height of all items in the adapter.
*
* Modified from http://iserveandroid.blogspot.com/2011/06/how-to-calculate-lsitviews-total.html
*
* #return
*/
private int getTotalItemHeight() {
ListAdapter adapter = getAdapter();
int listviewElementsheight = 0;
for(int i =0; i < adapter.getCount(); i++) {
View mView = adapter.getView(i, null, this);
mView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
listviewElementsheight+= mView.getMeasuredHeight();
}
return listviewElementsheight;
}
The full source code can be found here on GitHub.
I had a similar issue when getFirstVisiblePosition() returned number greater than getCount().
Fixed it by calling listView.setAdapter(listView.getAdapter()).
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);