Reputation: 6167
I'm trying to create a list that shows an indeterminate progress bar as the last entry while it's fetching more data. I can show the bar and get/add the data, but scrolling up and down while it's loading causes multiple progress bars to show up.
I have a ListActivity that uses an ArrayAdapter. Each row has a layout as follows.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:background="@drawable/textlines" android:padding="2dip">
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content" android:id="@+id/rowContent">
<TextView android:height="20sp" android:text=""
android:id="@+id/search_display" android:layout_width="fill_parent"
android:textSize="16sp" android:layout_height="20sp" android:gravity="left" />
</LinearLayout>
<LinearLayout android:layout_height="wrap_content"
android:layout_width="fill_parent" android:id="@+id/rowSpinner"
android:padding="3px" android:gravity="center" android:visibility="gone">
<ProgressBar android:layout_width="wrap_content"
android:layout_height="wrap_content" android:id="@+id/progress"
android:indeterminate="true" />
</LinearLayout>
</RelativeLayout>
The ListView has an OnScrollListener with the following onScroll method.
public void onScroll(final AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
{
// detect if last item is visible
if ((visibleItemCount < totalItemCount)
&& (firstVisibleItem + visibleItemCount == totalItemCount))
{
if (false == scrollTaskRunning)
{
scrollTaskRunning = true;
getMoreData(totalItemCount);
}
}
}
getMoreData invokes an AsyncTask that gets some more data to add to adapter. In its onPreExecute I call showSpinner() --
private void showSpinner()
{
// nothing to do if there's already a spinner visible
if (isSpinVisible == true) return;
// hide the progress spinner
if (0 < lvList.getChildCount())
{
View vRow = lvList.getChildAt(lvList.getChildCount() - 1);
vRow.findViewById(R.id.rowContent).setVisibility(View.GONE);
vRow.findViewById(R.id.rowSpinner).setVisibility(View.VISIBLE);
}
isSpinVisible = true;
}
and in its onPostExecute/onCancelled I call hideSpinner() which is the same code, except checking the isSpinVisible flag the other way, and GONE and VISIBLE swapped. The swapping code, so far as I can tell, only gets called once, but multiple entries in the last show up with the progress bar visible if you scroll up and down.
I tried doing this instead for hideSpinner() --
private void hideSpinner()
{
// nothing to do if there's no spinner visible
if (isSpinVisible == false) return;
// show the progress spinner
int iChildCount = lvList.getChildCount();
if (0 < lvList.getChildCount())
{
for (int i = 0; i < iChildCount; i++)
{
View vRow = lvList.getChildAt(iChildCount);
if (null != vRow)
{
vRow.findViewById(R.id.rowContent).setVisibility(View.VISIBLE);
vRow.findViewById(R.id.rowSpinner).setVisibility(View.GONE);
}
}
}
else if (null != pbSearch)
{
pbSearch.setVisibility(View.GONE);
}
isSpinVisible = false;
}
but vRow is null, and some of the progress bars still show up. How do I fix this? Alternatively, is there a better way to do this? (I thought I might be able to do something with my ArrayAdapter's getView()
method, but I couldn't work it out.)
ETA: this answer seems to explain the problem I'm having, but knowing that hasn't helped me find a way around it.
ETA2: I tried doing this:
final LayoutInflater mInflater = (LayoutInflater)
getSystemService(Context.LAYOUT_INFLATER_SERVICE);
aapSearchResults = new ArrayAdapter<ParsedXML>(this, R.layout.search_row, saData)
{
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
View row;
// get the view
if (null == convertView)
{
row = mInflater.inflate(R.layout.search_row, null);
}
else
{
row = convertView;
}
// bind the data to the view
TextView tv = (TextView) row.findViewById(R.id.search_display);
tv.setText(getItem(position).name);
// show data, hide spinner
row.findViewById(R.id.rowContent).setVisibility(View.VISIBLE);
row.findViewById(R.id.rowSpinner).setVisibility(View.GONE);
// if the current position is the last, and a task is running,
// show the progress bar
if (taskRunning && (position == this.getCount()-1))
{
row.findViewById(R.id.rowContent).setVisibility(View.GONE);
row.findViewById(R.id.rowSpinner).setVisibility(View.VISIBLE);
}
return row;
}
};
lvList.setAdapter(aapSearchResults);
but I'm clearly making a logic error still, because sometimes scrolling up and down now gets me blank entries. (Is it because the count has changed?)
Upvotes: 0
Views: 5571
Reputation: 1516
I would suggest you use a BaseAdapter instead. It automatically loads more elements when the last one on the list is shown.
Upvotes: 0
Reputation: 10129
Generally speaking you should never touch the children of a ListView
directly. In fact, after they leave your adapter's getView()
nest, you should consider them to be fully independent and entirely divorced from your direct control. The only way you can control them is by calling notifyDataSetChanged()
and letting the ListView make new ones.
To get around your problem, do something like:
public class MyAdapter extends ArrayAdapter[…]{
//[…]
private boolean mIsLoading = false;
public void setIsLoading(boolean isLoading){
if (mIsLoading != isLoading){
mIsLoading = isLoading;
notifyDataSetChanged();
}
}
@Override
public int getCount(){
return super.getCount() + (isLoading ? 1 : 0);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (mIsLoading && position == (getCount() - 1)){
//return your progress view goes here. Ensure that it has the ID R.id.progress;
}else{
if (convertView != null && convertView.getId() == R.id.progress){
convertView = null;
}
return super.getView(position, convertView, parent);
}
}
}
Upvotes: 2