在 ListView 中检测向上滚动和向下滚动

71
我有以下需求:
  • 首先,从服务器获取第2页的数据,并在ListView中填充项目。

考虑到在某种情况下同时存在上一页和下一页,因此添加了以下代码:

 if(prevPageNo > 0){
    mListViewActual.setOnScrollListener(this);
 }

 if(nextPageNo > 0){
    mListViewActual.setOnScrollListener(this);
 }

以下是检测滚动向上和向下的条件,适用于以下方法:

  1. void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
  2. void onScrollStateChanged(AbsListView view, int scrollState)

在检测到滚动向上或向下后,将调用相应的服务,并传递前一页或后一页的页码,以获取要在Listview中填充的项目。

任何输入都将有所帮助。

已查看以下链接,但未返回正确的滚动向上/向下操作:

link 1 link 2

17个回答

94

尝试使用setOnScrollListener并实现onScrollStateChanged和scrollState。

setOnScrollListener(new OnScrollListener(){
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
      // TODO Auto-generated method stub
    }
    public void onScrollStateChanged(AbsListView view, int scrollState) {
      // TODO Auto-generated method stub
      final ListView lw = getListView();

       if(scrollState == 0) 
      Log.i("a", "scrolling stopped...");


        if (view.getId() == lw.getId()) {
        final int currentFirstVisibleItem = lw.getFirstVisiblePosition();
         if (currentFirstVisibleItem > mLastFirstVisibleItem) {
            mIsScrollingUp = false;
            Log.i("a", "scrolling down...");
        } else if (currentFirstVisibleItem < mLastFirstVisibleItem) {
            mIsScrollingUp = true;
            Log.i("a", "scrolling up...");
        }

        mLastFirstVisibleItem = currentFirstVisibleItem;
    } 
    }
  });

2
如果您想在滚动状态为“SCROLL_STATE_FLING”或“SCROLL_STATE_TOUCH_SCROLL”时确定滚动方向,该怎么办?即使滚动状态尚未达到“SCROLL_STATE_IDLE”。 - Etienne Lawlor
4
我适用的方法是在OnScroll中添加这段代码,而不是在onScrollStateChanged中添加。 - Ayman Mahgoub
如果所有项目都可见,则无法工作,例如您有2个项目并且希望像WhatsApp一样向上滚动以加载更多。 - IulianT

63

以下是一些解决方案中工作的修改版本。

添加另一个类ListView:

package com.example.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AbsListView;

public class ListView extends android.widget.ListView {

    private OnScrollListener onScrollListener;
    private OnDetectScrollListener onDetectScrollListener;

    public ListView(Context context) {
        super(context);
        onCreate(context, null, null);
    }

    public ListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        onCreate(context, attrs, null);
    }

    public ListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        onCreate(context, attrs, defStyle);
    }

    @SuppressWarnings("UnusedParameters")
    private void onCreate(Context context, AttributeSet attrs, Integer defStyle) {
        setListeners();
    }

    private void setListeners() {
        super.setOnScrollListener(new OnScrollListener() {

            private int oldTop;
            private int oldFirstVisibleItem;

            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                if (onScrollListener != null) {
                    onScrollListener.onScrollStateChanged(view, scrollState);
                }
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                if (onScrollListener != null) {
                    onScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
                }

                if (onDetectScrollListener != null) {
                    onDetectedListScroll(view, firstVisibleItem);
                }
            }

            private void onDetectedListScroll(AbsListView absListView, int firstVisibleItem) {
                View view = absListView.getChildAt(0);
                int top = (view == null) ? 0 : view.getTop();

                if (firstVisibleItem == oldFirstVisibleItem) {
                    if (top > oldTop) {
                        onDetectScrollListener.onUpScrolling();
                    } else if (top < oldTop) {
                        onDetectScrollListener.onDownScrolling();
                    }
                } else {
                    if (firstVisibleItem < oldFirstVisibleItem) {
                        onDetectScrollListener.onUpScrolling();
                    } else {
                        onDetectScrollListener.onDownScrolling();
                    }
                }

                oldTop = top;
                oldFirstVisibleItem = firstVisibleItem;
            }
        });
    }

    @Override
    public void setOnScrollListener(OnScrollListener onScrollListener) {
        this.onScrollListener = onScrollListener;
    }

    public void setOnDetectScrollListener(OnDetectScrollListener onDetectScrollListener) {
        this.onDetectScrollListener = onDetectScrollListener;
    }
}

还有一个接口:

public interface OnDetectScrollListener {

    void onUpScrolling();

    void onDownScrolling();
}

最后是如何使用:

com.example.view.ListView listView = (com.example.view.ListView) findViewById(R.id.list);
listView.setOnDetectScrollListener(new OnDetectScrollListener() {
    @Override
    public void onUpScrolling() {
        /* do something */
    }

    @Override
    public void onDownScrolling() {
        /* do something */
    }
});

在你的XML布局中:

<com.example.view.ListView
    android:id="@+id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

这是我的第一个话题,请不要严苛评判我。=)


这正是我一直在寻找的。运行得非常好!点赞! - Nari Kim Shin
这非常精确!!谢谢。 - Krit
1
为什么不扩展滚动监听器而不是整个视图?仅为滚动事件扩展视图并不是一个好的做法,特别是如果以后想要附加新的滚动监听器... - mradzinski

47

这是一个简单的实现:

lv.setOnScrollListener(new OnScrollListener() {
        private int mLastFirstVisibleItem;

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

        }

        @Override
        public void onScroll(AbsListView view, int firstVisibleItem,
                int visibleItemCount, int totalItemCount) {

            if(mLastFirstVisibleItem<firstVisibleItem)
            {
                Log.i("SCROLLING DOWN","TRUE");
            }
            if(mLastFirstVisibleItem>firstVisibleItem)
            {
                Log.i("SCROLLING UP","TRUE");
            }
            mLastFirstVisibleItem=firstVisibleItem;

        }
    });

如果您需要更高的精度,您可以使用此自定义ListView类:

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AbsListView;
import android.widget.ListView;

/**
 * Created by root on 26/05/15.
 */
public class ScrollInterfacedListView extends ListView {


    private OnScrollListener onScrollListener;
    private OnDetectScrollListener onDetectScrollListener;

    public ScrollInterfacedListView(Context context) {
        super(context);
        onCreate(context, null, null);
    }

    public ScrollInterfacedListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        onCreate(context, attrs, null);
    }

    public ScrollInterfacedListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        onCreate(context, attrs, defStyle);
    }

    @SuppressWarnings("UnusedParameters")
    private void onCreate(Context context, AttributeSet attrs, Integer defStyle) {
        setListeners();
    }

    private void setListeners() {
        super.setOnScrollListener(new OnScrollListener() {

            private int oldTop;
            private int oldFirstVisibleItem;

            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                if (onScrollListener != null) {
                    onScrollListener.onScrollStateChanged(view, scrollState);
                }
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                if (onScrollListener != null) {
                    onScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
                }

                if (onDetectScrollListener != null) {
                    onDetectedListScroll(view, firstVisibleItem);
                }
            }

            private void onDetectedListScroll(AbsListView absListView, int firstVisibleItem) {
                View view = absListView.getChildAt(0);
                int top = (view == null) ? 0 : view.getTop();

                if (firstVisibleItem == oldFirstVisibleItem) {
                    if (top > oldTop) {
                        onDetectScrollListener.onUpScrolling();
                    } else if (top < oldTop) {
                        onDetectScrollListener.onDownScrolling();
                    }
                } else {
                    if (firstVisibleItem < oldFirstVisibleItem) {
                        onDetectScrollListener.onUpScrolling();
                    } else {
                        onDetectScrollListener.onDownScrolling();
                    }
                }

                oldTop = top;
                oldFirstVisibleItem = firstVisibleItem;
            }
        });
    }

    @Override
    public void setOnScrollListener(OnScrollListener onScrollListener) {
        this.onScrollListener = onScrollListener;
    }

    public void setOnDetectScrollListener(OnDetectScrollListener onDetectScrollListener) {
        this.onDetectScrollListener = onDetectScrollListener;
    }


    public interface OnDetectScrollListener {

        void onUpScrolling();

        void onDownScrolling();
    }

}

一个使用示例: (不要忘记在你的layout.xml中将其添加为一个Xml标签)
scrollInterfacedListView.setOnDetectScrollListener(new ScrollInterfacedListView.OnDetectScrollListener() {
            @Override
            public void onUpScrolling() {
               //Do your thing
            }

            @Override
            public void onDownScrolling() {

             //Do your thing
            }
        });

基本上对于小滚动不起作用。这实现了一个“一次一个”的检测器。只有当您滚动“整个单元格”时才会触发。 - Fattie
我相信这个代码完美运行(记住它是基于单元格而不是像素)。我进行了广泛的测试,与下面的“四个测试”方法进行了比较(例如Elulici的答案)。希望能对某些人有所帮助。 - Fattie
1
伟大而简单的解决方案。 - LJ Wilson
太棒了!完美运行! - KinGPinG
我得到了最简单的解决方案,谢谢。 - Chintan Desai

12

虽然所有的方法都已发布,但是依然存在一些问题,比如无法识别用户是否从第一个元素向上滚动或者从最后一个元素向下滚动。 这里提供另一种检测滚动方向的方法:

        listView.setOnTouchListener(new View.OnTouchListener() {
            float height;
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();
                float height = event.getY();
                if(action == MotionEvent.ACTION_DOWN){
                    this.height = height;
                }else if(action == MotionEvent.ACTION_UP){
                    if(this.height < height){
                        Log.v(TAG, "Scrolled up");
                    }else if(this.height > height){
                        Log.v(TAG, "Scrolled down");
                    }
                }
                return false;
            }
        });

这会防止ListView子项的触摸还是点击? - Noor Hossain

9
            ListView listView = getListView();
            listView.setOnScrollListener(new OnScrollListener() {

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

                    view.setOnTouchListener(new OnTouchListener() {
                        private float mInitialX;
                        private float mInitialY;

                        @Override
                        public boolean onTouch(View v, MotionEvent event) {
                            switch (event.getAction()) {
                                case MotionEvent.ACTION_DOWN:
                                    mInitialX = event.getX();
                                    mInitialY = event.getY();
                                    return true;
                                case MotionEvent.ACTION_MOVE:
                                    final float x = event.getX();
                                    final float y = event.getY();
                                    final float yDiff = y - mInitialY;
                                    if (yDiff > 0.0) {
                                        Log.d(tag, "SCROLL DOWN");
                                        scrollDown = true;
                                        break;

                                    } else if (yDiff < 0.0) {
                                        Log.d(tag, "SCROLL up");
                                        scrollDown = true;
                                        break;

                                    }
                                    break;
                            }
                            return false;
                        }
                    });

这确实是最好的解决方案,因为所有其他解决方案都依赖于当前和最后可见项。当依赖于列表项时,你也依赖于适配器,如果你滚动而适配器没有充气下一个项,你就不知道用户是向上还是向下滚动。 - Koc
3
这个解决方案非常糟糕,因为它在每次 onScrollStateChanged 中设置视图触摸监听器,这会导致很多额外的开销。 - Sadegh

8

我的解决方案完美地工作,为每个滚动方向提供精确的值。 distanceFromFirstCellToTop 包含从第一个单元格到父视图顶部的精确距离。我将这个值保存在 previousDistanceFromFirstCellToTop 中,当我滚动时,将其与新值进行比较。如果它更低,则我向上滚动,否则,我向下滚动。

private int previousDistanceFromFirstCellToTop;

listview.setOnScrollListener(new OnScrollListener() {

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

    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        View firstCell = listview.getChildAt(0);
        int distanceFromFirstCellToTop = listview.getFirstVisiblePosition() * firstCell.getHeight() - firstCell.getTop();

        if(distanceFromFirstCellToTop < previousDistanceFromFirstCellToTop)
        {
           //Scroll Up
        }
        else if(distanceFromFirstCellToTop  > previousDistanceFromFirstCellToTop)
        {
           //Scroll Down
        }
        previousDistanceFromFirstCellToTop = distanceFromFirstCellToTop;
    }
});

对于Xamarin开发人员,解决方案如下:
注意:不要忘记在UI线程上运行。
listView.Scroll += (o, e) =>
{
    View firstCell = listView.GetChildAt(0);
    int distanceFromFirstCellToTop = listView.FirstVisiblePosition * firstCell.Height - firstCell.Top;

    if (distanceFromFirstCellToTop < previousDistanceFromFirstCellToTop)
    {
        //Scroll Up
    }
    else if (distanceFromFirstCellToTop > previousDistanceFromFirstCellToTop)
    {
        //Scroll Down
    }
    previousDistanceFromFirstCellToTop = distanceFromFirstCellToTop;
};

1
好的回答!我只需要添加一个检查,看看第一个单元格是否为空(因为它可能还没有被填充)。 - peshkira
这是我找到的关于检测滚动方向的最佳且最简单的答案!即使项目高度小或大,它也能正常、稳定地工作。非常感谢。 顺便说一下,只是一个小问题,你可以将previousDistanceFromFirstCellToTop变量放在内部匿名的OnScrollListener类中以使其更好。 - Phong Nguyen
1
很棒的解决方案!只有一件事情缺失: 在 View firstCell = view.getChildAt(0); 之后添加:if(firstCell == null){ return; } - Mark Kazakov

4
为了在较大的元素中检测滚动,我倾向于使用onTouch监听器:
listview.setOnTouchListener(new View.OnTouchListener() {

        int scrollEventListSize = 5; 
        float lastY;
        // Used to correct for occasions when user scrolls down(/up) but the onTouchListener detects it incorrectly. We will store detected up-/down-scrolls with -1/1 in this list and evaluate later which occured more often
        List<Integer> downScrolledEventsHappened;

        @Override
        public boolean onTouch(View v, MotionEvent event) {

            float diff = 0;
            if(event.getAction() == event.ACTION_DOWN){
                lastY = event.getY();
                downScrolledEventsHappened = new LinkedList<Integer>();
            }
            else if(event.getAction() == event.ACTION_MOVE){
                diff = event.getY() - lastY;
                lastY = event.getY();

                if(diff>0)
                    downScrolledEventsHappened.add(1);
                else 
                    downScrolledEventsHappened.add(-1);

               //List needs to be filled with some events, will happen very quickly
                if(downScrolledEventsHappened.size() == scrollEventListSize+1){
                    downScrolledEventsHappened.remove(0);
                    int res=0;
                    for(int i=0; i<downScrolledEventsHappened.size(); i++){
                        res+=downScrolledEventsHappened.get(i);
                    }

                    if (res > 0) 
                        Log.i("INFO", "Scrolled up");
                    else 
                        Log.i("INFO", "Scrolled down");
                }
            }
            return false; // don't interrupt the event-chain
        }
    });

4
我使用了更简单的解决方案:
setOnScrollListener( new OnScrollListener() 
{

    private int mInitialScroll = 0;

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem,
            int visibleItemCount, int totalItemCount) 
    {
        int scrolledOffset = computeVerticalScrollOffset();

        boolean scrollUp = scrolledOffset > mInitialScroll;
        mInitialScroll = scrolledOffset;
    }


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


    }

}

什么是computeVerticalScrollOffset? - Fattie
2
ListView中的受保护方法 - Vaiden
非常感谢您的快速回复。我似乎无法使用它:我只得到“找不到符号...”我尝试了所有的view.computeVerticalScrollOffset()、myListView.computeVerticalScrollOffset()和在您的代码中直接调用它。抱歉,我是从iOS转来的Android新手(我喜欢Android!) - Fattie
2
由于这是一个受保护的方法,您需要扩展ListView。 - Vaiden

4

只需将滚动监听器设置到您的列表视图中。

如果您有页眉或页脚,则还应检查可见计数。如果增加,则表示您正在向下滚动。(如果有页脚而不是页眉,请反转它)

如果您的列表视图中没有任何页眉或页脚,则可以删除检查可见项计数的行。

listView.setOnScrollListener(new AbsListView.OnScrollListener() {
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
            if (mLastFirstVisibleItem > firstVisibleItem) {
                Log.e(getClass().toString(), "scrolling up");
            } else if (mLastFirstVisibleItem < firstVisibleItem) {
                Log.e(getClass().toString(), "scrolling down");
            } else if (mLastVisibleItemCount < visibleItemCount) {
                Log.e(getClass().toString(), "scrolling down");
            } else if (mLastVisibleItemCount > visibleItemCount) {
                Log.e(getClass().toString(), "scrolling up");
            }
            mLastFirstVisibleItem = firstVisibleItem;
            mLastVisibleItemCount = visibleItemCount;
        }

        public void onScrollStateChanged(AbsListView listView, int scrollState) {
        }
    });

并且有这些变量
int mLastFirstVisibleItem;
int mLastVisibleItemCount;

只有当您移动“一个完整单元格”时,此功能才有效。它无法检测到小的移动。但它确实可靠。请注意,它会在单元格边框通过屏幕(或任何包围物)的底部时触发,而不是顶部。顺便说一句,伙计,我敢肯定你把上下搞反了! - Fattie
我看不出这个答案和更简单的方法(例如GaiRom)之间在功能上有任何区别。我进行了广泛的测试,包括边角情况等。Eluleci,你为什么要增加额外的比较呢? - Fattie

3

存储第一个可见项,然后在下一次滚动时检查新的第一个可见项是小于还是大于之前的。

示例伪代码(未经测试):

int lastVisibleItem = 0;
boolean isScrollingDown = false;

void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    if (firstVisibleItem > lastVisibleItem) {
        isScrollingDown = true;
    }
    else {
        isScrollingDown = false;
    }
    lastVisibleItem = firstVisibleItem;
}

实现了上述代码。当用户滚动并到达ListView的最后一个项目时,将调用onScroll(),但是当用户滚动到ScrollView的第一个可见项目时,不会调用onScroll()。对于第一页,第一个可见项目为0。如何处理这种情况?在两种情况下都会调用onScrollStateChanged()。 - chiranjib
我有一段代码,每次都会调用onScroll!你确定监听器一直在运行吗?无论任何条件,都要设置mListViewActual.setOnScrollListener(this)! - thiagolr
我已经添加了mListViewActual.setOnScrollListener(this)而没有任何条件。然而,当用户尝试向上滚动即超出此情况下的顶部可见项(为0)时,onScroll()并未被调用。目前,正如先前提到的,用户位于第2页。只有当用户尝试向上滚动,即超出第一个可见项且调用onScroll()时,才可以向服务器发起下一次服务调用以获取第1页的数据。 - chiranjib

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接