Android infinitely scrolling list in both directions
Asked Answered
L

1

6

I'm trying to figure out how to implement an infinitely scrolling list. It will display a calendar and events and it should start from now or selected date. It should be scrollable in both directions, past and future. The solutions with OnScrollListener here seem to work pretty well if I only need to go to future (index just grows bigger). But I don't see how I would go to the past.

This solution seems to be very wasteful for my case. getView is called thousands of times. Maybe ListView isn't the solution, and I'll have to go with lower-level code. Any ideas?

EDIT: getView being called thousands of times wasn't the fault of the latter solution. However, it still gets called too many times and with wrong values. If I set selection like this:

myList.setSelection(Integer.MAX_VALUE/2)

I get getView calls with indexes starting from zero. For example, I get getView calls like this:

getView pos 0
...
getView pos 26

and then

getView pos 1073741823
...
getView pos 1073741847

Which are the correct ones. Then:

getView pos 0
...
getView pos 26

again

This all happens before I scroll or touch the screen at all. Doesn't seem to make much sense.

Lycaon answered 16/3, 2014 at 9:3 Comment(5)
the second link you posted is a right solutionPipette
When testing it, my adapter's getView was called 2625 times before I even scrolled. That's pretty heavy especially considering that I need to load calendar events which means IO. Any idea how can I reduce the getView calls just to the visible area or something which is slightly larger?Lycaon
so you used github.com/commonsguy/cwac-endless and it works that bad ?Pipette
No, I didn't use cwac. I am under the impression that it infinitely grows only in one direction. Am I wrong?Lycaon
Almost a year later, what did you end up using @Lycaon ?Esp
D
1

Here is an implementation of this task.

EndlessScrollBaseAdapter.java

package com.example.endlessscrollinbothdirections;

import java.util.Map;
import android.content.Context;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.BaseAdapter;
import android.widget.TextView;

/** A child class shall subclass this Adapter and implement method getDataRow(int position,
 * View convertView, ViewGroup parent), which supplies a View present data in a ListRow.
 * This parent Adapter takes care of displaying ProgressBar in a row or indicating that it
 * has reached the last row. */
public abstract class EndlessScrollBaseAdapter<T> extends BaseAdapter implements
        OnScrollListener {
    private int mVisibleThreshold = 5;
    // the main data structure to save loaded data
    protected Map<Integer, T> mItems;
    protected Context mContext;
    // the serverListSize is the total number of items on the server side,
    // which should be returned from the web request results
    protected int mServerListSize = -1;
    // Two view types which will be used to determine whether a row should be displaying
    // data or a Progressbar
    public static final int VIEW_TYPE_LOADING = 0;
    public static final int VIEW_TYPE_ACTIVITY = 1;
    public static final int VIRTUAL_MIDDLE_OFFSET = Integer.MAX_VALUE / 2;

    public EndlessScrollBaseAdapter(Context context, Map<Integer, T> items) {
        mContext = context;
        mItems = items;
    }

    public void setServerListSize(int serverListSize) {
        this.mServerListSize = serverListSize;
    }

    /** disable click events on indicating rows */
    @Override
    public boolean isEnabled(int position) {
        return getItemViewType(position) == EndlessScrollBaseAdapter.VIEW_TYPE_ACTIVITY;
    }

    /** One type is normal data row, the other type is Progressbar */
    @Override
    public int getViewTypeCount() {
        return 2;
    }

    /** the size of the List plus one, the one is the last row, which displays a
     * Progressbar */
    @Override
    public int getCount() {
        return Integer.MAX_VALUE;
    }

    /** return the type of the row, the last row indicates the user that the ListView is
     * loading more data */
    @Override
    public int getItemViewType(int position) {
        return mItems.containsKey(position
                - EndlessScrollBaseAdapter.VIRTUAL_MIDDLE_OFFSET) ? EndlessScrollBaseAdapter.VIEW_TYPE_ACTIVITY
                : EndlessScrollBaseAdapter.VIEW_TYPE_LOADING;
    }

    @Override
    public T getItem(int position) {
        return mItems.get(position - EndlessScrollBaseAdapter.VIRTUAL_MIDDLE_OFFSET);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    /** returns the correct view */
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (getItemViewType(position) == EndlessScrollBaseAdapter.VIEW_TYPE_LOADING) {
            return getFooterView(position, convertView, parent);
        }
        return getDataRow(position, convertView, parent);
    };

    /** A subclass should override this method to supply the data row.
     *
     * @param position
     * @param convertView
     * @param parent
     * @return */
    public abstract View getDataRow(int position, View convertView, ViewGroup parent);

    /** returns a View to be displayed in the last row.
     *
     * @param position
     * @param convertView
     * @param parent
     * @return */
    public View getFooterView(int position, View convertView, ViewGroup parent) {
        if (position >= mServerListSize && mServerListSize > 0) {
            // the ListView has reached the last row
            TextView tvLastRow = new TextView(mContext);
            tvLastRow.setHint("Reached the last row.");
            tvLastRow.setGravity(Gravity.CENTER);
            return tvLastRow;
        } else {
            TextView tvLastRow = new TextView(mContext);
            tvLastRow.setHint("Loading...\n position: " + position);
            tvLastRow.setGravity(Gravity.CENTER);
            return tvLastRow;
        }
    }

    // Defines the process for actually loading more data based on page
    public abstract void onLoadMore(int virtualPosition);

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
            int totalItemCount) {
        for (int i = -mVisibleThreshold; i < visibleItemCount + mVisibleThreshold; i++) {
            int virtualPosition = firstVisibleItem
                    - EndlessScrollBaseAdapter.VIRTUAL_MIDDLE_OFFSET + i;
            onLoadMore(virtualPosition);
        }
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
    }
}

EndlessScrollAdapter.java

package com.example.endlessscrollinbothdirections;

import java.util.Map;
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class EndlessScrollAdapter extends EndlessScrollBaseAdapter<Integer> {
    public EndlessScrollAdapter(Activity activity, Map<Integer, Integer> list) {
        super(activity, list);
    }

    @Override
    public View getDataRow(int position, View convertView, ViewGroup parent) {
        TextView TextView;
        if (convertView == null) {
            TextView = new TextView(mContext);
        } else {
            TextView = (TextView) convertView;
        }
        TextView.setText("virtualPosition: "
                + (position - EndlessScrollBaseAdapter.VIRTUAL_MIDDLE_OFFSET) + "\n"
                + "row data: "
                + mItems.get(position - EndlessScrollBaseAdapter.VIRTUAL_MIDDLE_OFFSET));
        return TextView;
    }

    @Override
    public void onLoadMore(int virtualPosition) {
        // here you might launch an AsyncTask instead
        if (!mItems.containsKey(virtualPosition)) {
            mItems.put(virtualPosition, virtualPosition);
            notifyDataSetChanged();
        }
    }
}

MainActivity.java

package com.example.endlessscrollinbothdirections;

import java.util.HashMap;
import java.util.Map;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.widget.ListView;

public class MainActivity extends ActionBarActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ListView listView = (ListView) findViewById(R.id.lvItems);
        Map<Integer, Integer> items = new HashMap<Integer, Integer>();
        EndlessScrollAdapter endlessScrollAdapter = new EndlessScrollAdapter(this, items);
        listView.setAdapter(endlessScrollAdapter);
        listView.setSelection(EndlessScrollBaseAdapter.VIRTUAL_MIDDLE_OFFSET);
        listView.setOnScrollListener(endlessScrollAdapter);
    }
}

activity_main.xml

<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/lvItems"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >
</ListView>
Dormant answered 15/7, 2015 at 11:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.