RecyclerView的滚动事件OnScrollListener研究

(1)滚动事件分类

列表的滚动一般分为两种:

 1.手指按下 -> 手指拖拽列表移动 -> 手指停止拖拽 -> 抬起手指
 2.手指按下 -> 手指快速拖拽后抬起手指 -> 列表继续滚动 -> 停止滚动

上面的过程的状态变化如下:

1.静止 -> 被迫拖拽移动 -> 静止
2.静止 -> 被迫拖拽移动 -> 自己滚动 -> 静止

(2)监听RecyclerView的滚动

有两种方式可以监听滚动事件:

  1.setOnScrollListener(OnScrollListener listener)  //被废弃
  2.addOnScrollListener(OnScrollListener listener)

其中 setOnScrollListener 由于可能出现空指针的风险,已经过时。建议用addOnScrollListener。

(3)OnScrollListener

/**
 * An OnScrollListener can be added to a RecyclerView to receive messages when a scrolling event
 * has occurred on that RecyclerView.
 * <p>
 * @see RecyclerView#addOnScrollListener(OnScrollListener)
 * @see RecyclerView#clearOnChildAttachStateChangeListeners()
 *
 */
public abstract static class OnScrollListener {
    /**
     * Callback method to be invoked when RecyclerView's scroll state changes.
     *
     * @param recyclerView The RecyclerView whose scroll state has changed.
     * @param newState     The updated scroll state. One of {@link #SCROLL_STATE_IDLE},
     *                     {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}.
     */
    public void onScrollStateChanged(RecyclerView recyclerView, int newState){}

    /**
     * Callback method to be invoked when the RecyclerView has been scrolled. This will be
     * called after the scroll has completed.
     * <p>
     * This callback will also be called if visible item range changes after a layout
     * calculation. In that case, dx and dy will be 0.
     *
     * @param recyclerView The RecyclerView which scrolled.
     * @param dx The amount of horizontal scroll.
     * @param dy The amount of vertical scroll.
     */
    public void onScrolled(RecyclerView recyclerView, int dx, int dy){}
}

OnScrollListener类是个抽象类,有两个方法:

void onScrollStateChanged(RecyclerView recyclerView, int newState): 滚动状态变化时回调
void onScrolled(RecyclerView recyclerView, int dx, int dy): 滚动时回调

3.1 onScrollStateChanged(RecyclerView recyclerView, int newState)方法

回调的两个变量的含义:
recyclerView: 当前在滚动的RecyclerView
newState: 当前滚动状态.

其中newState有三种值:

/**
 * The RecyclerView is not currently scrolling.(静止没有滚动)
 */
public static final int SCROLL_STATE_IDLE = 0;

/**
 * The RecyclerView is currently being dragged by outside input such as user touch input.
 *(正在被外部拖拽,一般为用户正在用手指滚动)
 */
public static final int SCROLL_STATE_DRAGGING = 1;

/**
 * The RecyclerView is currently animating to a final position while not under outside control.
 *(自动滚动)
 */
public static final int SCROLL_STATE_SETTLING = 2;

3.2 onScrolled(RecyclerView recyclerView, int dx, int dy)方法

回调的三个变量含义:
recyclerView : 当前滚动的view
dx : 水平滚动距离
dy : 垂直滚动距离

dx > 0 时为手指向左滚动,列表滚动显示右面的内容
dx < 0 
时为手指向右滚动,列表滚动显示左面的内容
dy > 0 
时为手指向上滚动,列表滚动显示下面的内容
dy < 0 
时为手指向下滚动,列表滚动显示上面的内容

(4)canScrollVertically和canScrollHorizontally方法

public boolean canScrollVertically (int direction)
这个方法是判断View在竖直方向是否还能向上,向下滑动。
其中,direction为 -1 表示手指向下滑动(屏幕向上滑动), 1 表示手指向上滑动(屏幕向下滑动)。

public boolean canScrollHorizontally (int direction)
这个方法用来判断 水平方向的滑动

例如:
RecyclerView.canScrollVertically(1)的值表示是否能向下滚动,false表示已经滚动到底部
RecyclerView.canScrollVertically(-1)的值表示是否能向上滚动,false表示已经滚动到顶部

(5)两种判断是否到底部的方法:

5.1方法一:

如果 当前
第一个可见item的位置 + 当前可见的item个数 >= item的总个数
这样就可以判断出来,是在底部了。

loadingMoreListener = new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
        }

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);

            if (dy > 0) //向下滚动
            {
                int visibleItemCount = mLinearLayoutManager.getChildCount();	//得到显示屏幕内的list数量
                int totalItemCount = mLinearLayoutManager.getItemCount();	//得到list的总数量
                int pastVisiblesItems = mLinearLayoutManager.findFirstVisibleItemPosition();//得到显示屏内的第一个list的位置数position

                if (!loading && (visibleItemCount + pastVisiblesItems) >= totalItemCount) {
                    loading = true;
                    loadMoreDate();
                }
            }
        }
};

通过
visibleItemCount + pastVisiblesItems) >= totalItemCount
来判断是否是底部。

5.2方法二:

通过canScrollVertically 来判断

loadingMoreListener = new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        if(!loading && !recyclerView.canScrollVertically(1)){
            loading = true;
            loadMoreDate();
        }
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);

//                if (dy > 0) //向下滚动
//                {
//                    int visibleItemCount = mLinearLayoutManager.getChildCount();
//                    int totalItemCount = mLinearLayoutManager.getItemCount();
//                    int pastVisiblesItems = mLinearLayoutManager.findFirstVisibleItemPosition();
//
//                    if (!loading && (visibleItemCount + pastVisiblesItems) >= totalItemCount) {
//                        loading = true;
//                        loadMoreDate();
//                    }
//                }
    }
};

 应用的一个共同的特点就是当用户欢动时自动加载更多的内容,这是通过用户滑动触发一定的阈值时发送数据请求实现的。 
相同的是:信息实现滑动的效果需要定义在列表中最后一个可见项,和某些类型的阈值以便于开始在最后一项到达之前开始抓取数据,实现无限的滚动。 
实现无限滚动的现象的重要之处就在于在用户滑动到最低端之前就行数据的获取,所以需要加上一个阈值来帮助实现获取数据的预期。

使用ListView和GridView实现

每个AdapterView 例如ListView 和GridView 当用户开始进行滚动操作时候都会触发OnScrollListener .使用这个系统我们就可以定义一个基本的EndlessScrollListener ,通过创造继承OnScrollListener 的类来支持大多数情况下的使用。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.codepath.customadapter;
 
import android.widget.AbsListView;
 
/**
  * Created by Administrator on 2016/7/11.
  */
public abstract class EndlessScrollListener implements AbsListView.OnScrollListener {
  //在你滑动项下最少为多少时开始加载数据
  private int visibleThreshold = 5 ;
  //已经加载数据的当前页码
  private int currentPage = 0 ;
  //上一次加载数据后数据库的数据量
  private int previousTotalItemCount = 0 ;
  //我们是否在等待最后一组数据的加载
  private boolean loading = true ;
  //设置开始页的下标
  private int startingPageIndex = 0 ;
  public EndlessScrollListener() {
 
  }
  public EndlessScrollListener( int visibleThreshold) {
   this .visibleThreshold = visibleThreshold;
  }
  public EndlessScrollListener( int visibleThreshold, int startingPageIndex) {
   this .visibleThreshold = visibleThreshold;
   this .startingPageIndex = startingPageIndex;
  }
  //这个方法可能会在滑动调用很多次,所以在设计时要保持谨慎
  //我们需要一些有用的参数来帮助我们,当我们需要加载更多数据的时候
  //但是我们首先要检查是否我们在等待先前的加载结束
 
//onScroll()当列表或网格视图被滚动后将会调用,参数一:报告状态的视图参数二:第一个可以看见的项的下标,参数三:可见项的数量参数四:listAdapter中所有的项数
  @Override
  public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
 
   //如果总项数为0,而且先前没有项,那么这个列表是无效的应该被设定为初始状态
   if (totalItemCount < previousTotalItemCount) {
    this .currentPage = this .startingPageIndex;
    this .previousTotalItemCount = totalItemCount;
    if (totalItemCount == 0 ) { this .loading = true ;}
   }
   //如果仍在加载中我们可以检查一下数据集合是否改变了,如果改变的话那就是已经完成了loading需要更新当前
   //页数和数据总量
   if (loading && (totalItemCount > previousTotalItemCount)) {
    loading = false ;
    previousTotalItemCount = totalItemCount;
    currentPage++;
   }
   //如果当前没有加载,我们需要检查当前是否达到了阈值,如果是的话我们需要
   //加载更多的数据,执行onLoadMore
   if (!loading && (firstVisibleItem + visibleItemCount + visibleThreshold) >= totalItemCount) {
    loading = onLoadMore(currentPage + 1 , totalItemCount);
   }
  }
  //定义实际加载数据的过程,如果数据加载完成返回false,如果正在加载返回true;
  public abstract boolean onLoadMore( int page, int totalItemCount);
 
  @Override
  public void onScrollStateChanged(AbsListView view, int scrollState) {
   //不采取动作
  }
}

要注意的是这是一个抽象的类,为了要使用这些,必须继承这个基本的类并且实现onLoadMore()方法实际的获取数据, 我们在一个活动中定义一个匿名的类来继承EndlessScrollListener然后将其连接AdapterView上

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MainActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstance) {
   //向平常一样
    ListView lvItems = (ListView) findViewById(R.id.lvItens);
    //将监听器绑定到上面
     lvItems.setOnScrollListener( new EndlessScrollListener() {
    @Override
    public boolean onLoadMore( int page, int totalItemsCount) {
     // 当新数据需要绑定到列表上的时候触发
     // 加载数据需要的代码Add whatever code is needed to append new items to your AdapterView
     customLoadMoreDataFromApi(page);
     // or customLoadMoreDataFromApi(totalItemsCount);
     return true ; //数据加载中为true,不然为false; ONLY if more data is actually being loaded; false otherwise.
    }
   });
  }
  //加载更多的数据
  public void customLoadMoreDataFromApi( int offset) {
   //这个方法通常会发起一些网络请求,然后向适配器添加更多的数据
   //将偏移量数据作为参数附在请求里来获得一个数据的分页
   //解析API返回的值并且获得新的对象构建适配器
  }
}

现在当用户滑动并且触发阈值时会自动触发onloadMore() 方法,而且监听器给予了对于页数和数据总量的访问权限。

实现RecyclerView的无限滑动

我们可以使用相同的方法来定义一个接口EndlessRecyclerViewScrollListener 然后定义一个onLoadMore() 的方法来进行实现。由于LayoutManager负责项的生成和滑动的管理,我们需要一个LayoutManage的实例来收集必要的信息。 
实现必要的分页需要这样的步骤: 
1).直接复制EndlessRecyclerViewScrollListener.java 
2).调用addOnScrollListener(...) 在RecyclerView 中来实现无限的分页,传递EndlessRecyclerViewScrollListener的实例来实现onLoadMore 方法来决定什么时候来加载新的数据 
3).在onLoadMore 方法中通过发送网络请求或者从源数据加载来获得更多item。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected void onCreate(Bundle savedInstanceState) {
   // Configure the RecyclerView获得RecylerView的实例
   RecyclerView rvItems = (RecyclerView) findViewById(R.id.rvContacts);
   LinearLayoutManager linearLayoutManager = new LinearLayoutManager( this );
   recyclerView.setLayoutManager(linearLayoutManager);
   // Add the scroll listener
   rvItems.addOnScrollListener( new EndlessRecyclerViewScrollListener(linearLayoutManager) {
    @Override
    public void onLoadMore( int page, int totalItemsCount) {
     // Triggered only when new data needs to be appended to the list
     // Add whatever code is needed to append new items to the bottom of the list
     customLoadMoreDataFromApi(page);
    }
   });
  }
 
  // Append more data into the adapter
  // This method probably sends out a network request and appends new data items to your adapter.
  public void customLoadMoreDataFromApi( int page) {
   // Send an API request to retrieve appropriate data using the offset value as a parameter.
   // --> Deserialize API response and then construct new objects to append to the adapter
   // --> Notify the adapter of the changes
  }
}

EndlessRecyclerView

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
  // The minimum amount of items to have below your current scroll position
  // before loading more.
  private int visibleThreshold = 5 ;
  // The current offset index of data you have loaded
  private int currentPage = 0 ;
  // The total number of items in the dataset after the last load
  private int previousTotalItemCount = 0 ;
  // True if we are still waiting for the last set of data to load.
  private boolean loading = true ;
  // Sets the starting page index
  private int startingPageIndex = 0 ;
 
  RecyclerView.LayoutManager mLayoutManager;
 
  public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager) {
   this .mLayoutManager = layoutManager;
  }
 
  public EndlessRecyclerViewScrollListener(GridLayoutManager layoutManager) {
   this .mLayoutManager = layoutManager;
   visibleThreshold = visibleThreshold * layoutManager.getSpanCount();
  }
 
  public EndlessRecyclerViewScrollListener(StaggeredGridLayoutManager layoutManager) {
   this .mLayoutManager = layoutManager;
   visibleThreshold = visibleThreshold * layoutManager.getSpanCount();
  }
 
  public int getLastVisibleItem( int [] lastVisibleItemPositions) {
   int maxSize = 0 ;
   for ( int i = 0 ; i < lastVisibleItemPositions.length; i++) {
    if (i == 0 ) {
     maxSize = lastVisibleItemPositions[i];
    }
    else if (lastVisibleItemPositions[i] > maxSize) {
     maxSize = lastVisibleItemPositions[i];
    }
   }
   return maxSize;
  }
 
  // This happens many times a second during a scroll, so be wary of the code you place here.
  // We are given a few useful parameters to help us work out if we need to load some more data,
  // but first we check if we are waiting for the previous load to finish.
  @Override
  public void onScrolled(RecyclerView view, int dx, int dy) {
   int lastVisibleItemPosition = 0 ;
   int totalItemCount = mLayoutManager.getItemCount();
 
   if (mLayoutManager instanceof StaggeredGridLayoutManager) {
    int [] lastVisibleItemPositions = ((StaggeredGridLayoutManager) mLayoutManager).findLastVisibleItemPositions( null );
    // get maximum element within the list
    lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions);
   } else if (mLayoutManager instanceof LinearLayoutManager) {
    lastVisibleItemPosition = ((LinearLayoutManager) mLayoutManager).findLastVisibleItemPosition();
   } else if (mLayoutManager instanceof GridLayoutManager) {
    lastVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findLastVisibleItemPosition();
   }
 
   // If the total item count is zero and the previous isn't, assume the
   // list is invalidated and should be reset back to initial state
   if (totalItemCount < previousTotalItemCount) {
    this .currentPage = this .startingPageIndex;
    this .previousTotalItemCount = totalItemCount;
    if (totalItemCount == 0 ) {
     this .loading = true ;
    }
   }
   // If it's still loading, we check to see if the dataset count has
   // changed, if so we conclude it has finished loading and update the current page
   // number and total item count.
   if (loading && (totalItemCount > previousTotalItemCount)) {
    loading = false ;
    previousTotalItemCount = totalItemCount;
   }
 
   // If it isn't currently loading, we check to see if we have breached
   // the visibleThreshold and need to reload more data.
   // If we do need to reload some more data, we execute onLoadMore to fetch the data.
   // threshold should reflect how many total columns there are too
   if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
    currentPage++;
    onLoadMore(currentPage, totalItemCount);
    loading = true ;
   }
  }
 
  // Defines the process for actually loading more data based on page
  public abstract void onLoadMore( int page, int totalItemsCount);
 
}

注意问题:
 1.对于ListView,确定将绑定监听器的步骤放在onCreate()的方法中
 2.为可加可靠的进行分页,需要确定在向列表中添加新数据的时候先清理适配器中的数据 
对于RecyclerView来说在通知适配器时推荐更细致的更新。
 3.对于RecyclerView来说确保在清除列表中的数据的时候迅速的通知适配器内容更新了,以便于可以触发新的onScroll事件,重置自己 

展示进度条

为了在底部展示进度条证明ListView正在加载。我们可以在Adapter中进行设置,我们可以定义两类,可以是进度条类型或者是文本表明达到了最底行,参考:http://guides.codepath.com/android/Endless-Scrolling-with-AdapterViews-and-RecyclerView

/*************************************百度平台的代码******************************************
 * Copyright 2016 stfalcon.com
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/

package com.baidu.speech.chatkit.message;

import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;

class RecyclerScrollMoreListener
        extends RecyclerView.OnScrollListener {

    private OnLoadMoreListener loadMoreListener;
    private int currentPage = 0;
    private int previousTotalItemCount = 0;
    private boolean loading = true;

    private RecyclerView.LayoutManager mLayoutManager;

    RecyclerScrollMoreListener(LinearLayoutManager layoutManager, OnLoadMoreListener loadMoreListener) {
        this.mLayoutManager = layoutManager;
        this.loadMoreListener = loadMoreListener;
    }

    private int getLastVisibleItem(int[] lastVisibleItemPositions) {
        int maxSize = 0;
        for (int i = 0; i < lastVisibleItemPositions.length; i++) {
            if (i == 0) {
                maxSize = lastVisibleItemPositions[i];
            } else if (lastVisibleItemPositions[i] > maxSize) {
                maxSize = lastVisibleItemPositions[i];
            }
        }
        return maxSize;
    }

    @Override
    public void onScrolled(RecyclerView view, int dx, int dy) {
        if (loadMoreListener != null) {
            int lastVisibleItemPosition = 0;
            int totalItemCount = mLayoutManager.getItemCount();

            if (mLayoutManager instanceof StaggeredGridLayoutManager) {
                int[] lastVisibleItemPositions =
                        ((StaggeredGridLayoutManager) mLayoutManager).findLastVisibleItemPositions(null);
                lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions);
            } else if (mLayoutManager instanceof LinearLayoutManager) {
                lastVisibleItemPosition = ((LinearLayoutManager) mLayoutManager).findLastVisibleItemPosition();
            } else if (mLayoutManager instanceof GridLayoutManager) {
                lastVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findLastVisibleItemPosition();
            }

            if (totalItemCount < previousTotalItemCount) {
                this.currentPage = 0;
                this.previousTotalItemCount = totalItemCount;
                if (totalItemCount == 0) {
                    this.loading = true;
                }
            }

            if (loading && (totalItemCount > previousTotalItemCount)) {
                loading = false;
                previousTotalItemCount = totalItemCount;
            }

            int visibleThreshold = 5;
            if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
                currentPage++;
                loadMoreListener.onLoadMore(currentPage, totalItemCount);
                loading = true;
            }
        }
    }

    interface OnLoadMoreListener {
        void onLoadMore(int page, int total);
    }
}


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值