long mode 分页_说说分页加载及下拉刷新

本文主要内容

分页加载

下拉刷新

分页加载,从服务器获取数据列表,列表长度很长,不可能一次获取完成,因为一次获取数据量太大影响处理速度,实际上用户也不可能一次性需要浏览这么多数据,所以需要分页加载,一次只获取有限条数目的数据量。

比如下图,是凤凰新闻的截图,用户进入应用时,只会加载有限数据的新闻,当用户往下翻时,当其检测到用户已经翻到结尾时,则加载另一页的新闻,不过此处为了显示效果,断网了,它才显示加载失败。

下拉刷新,如果用户对当前界面数据不感兴趣,需要查看其它数据,可以下拉刷新,从服务器重新获取数据。类似新闻类的app内都有这种功能。

分页加载

假设当前页面高度只够显示3个数据项,服务器上却有4个数据项,如果使用分页加载会是什么样呢?

第一屏

当用户滑动到尾端时,此时去请求服务器第4项数据。

第2屏

第一、二项数据会被隐藏掉,界面将显示第三项数据,第四项数据以及加载中的状态。当然,如果我们检测到服务器没有更多数据了,那我们还可以将加载中的状态改成已最新状态。

假设服务器的数据有7条,那此时,我们应该在第二屏看到的是第四、五、六条数据。

如果服务器的数据不足3条,只有一条的情况下,应该也只能显示第一项数据以及已最新状态。

上面的几种情况,都提到了一种情况,即是检测到用户已经下拉到尾端了,如何检测用户已经下拉到尾端呢?在此,我们以RecycleView为例:

private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {

@Override

public void onScrollStateChanged(RecyclerView recyclerView, int newState) {

super.onScrollStateChanged(recyclerView, newState);

RecyclerView.LayoutManager manager = mList.getLayoutManager();

if (manager instanceof LinearLayoutManager && newState == RecyclerView.SCROLL_STATE_IDLE) {

int lastCompletelyVisibleItemPosition = ((LinearLayoutManager) manager).findLastCompletelyVisibleItemPosition();

if (mAdapter.getItemCount() > Constant.PER_PAGE_COUNT &&

lastCompletelyVisibleItemPosition == mAdapter.getItemCount() - 1) {

loadNextPage();

}

}

}

};

RecycleView使用LayoutManager作为布局管理器,而LayoutManager有接口findLastCompletelyVisibleItemPosition可知道RecycleView上最后可视项的position,将position和RecycleView显示的总数据数量一比,如果一致,那么用户就将RecycleView滑到底了。

上文提到的假设,还有一个问题没有解决,貌似滑动列表中有两种类型的数据项,一种是正常的数据项,另一种则是加载中等状态项,如何在列表中同时显示两种类型的数据呢?

其中这个挺简单的,因为列表(不论是RecycleView还是ListView)中的每一个数据项都对应着一个布局文件,如果我们将显示的条目分类,一类对应着正常的数据项布局文件,另一类对应着状态项布局文件即可。

RecycleView可定制化更好,看看它的adapter对应的方法:

public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

if (viewType == Constant.ITEM_TYPE_DATA){

//处理数据项

}else if (viewType == Constant.ITEM_TYPE_MORE){

//处理状态项

}

return null;

}

onCreateViewHolder方法中自带参数viewType,根据此参数,分门另类处理不同条目即可。有同学可能要问了,在哪里给条目设置viewType呢?需要重写getItemViewType方法即可。

public int getItemViewType(int position) {

if (!mDatas.isEmpty() && position >= mDatas.size()){

return Constant.ITEM_TYPE_MORE;

}else {

return Constant.ITEM_TYPE_DATA;

}

}

还有一个小点要处理,记得在重写getItemCount玩点小花招,状态项才能显示出来,要显示的条目肯定是真实数据条目再加1个状态条目才对,所以getItemCount这么重写:

public int getItemCount() {

//如果数据大于每一屏显示的数据个数,则显示多一个,加载更多

int size = mDatas.size();

//下拉到最下边,加载中

if (size < Constant.PER_PAGE_COUNT){

size = size;

}else {

size ++;

}

return size;

}

现在,还剩下最后一个问题,理论上状态项有三种不同的状态:

加载中

已最新(没有更多数据可以加载了)

加载异常,点击重新加载

这三种状态如何切换呢?

给adapter增加一个变量,根据数据加载的情况,对adapter赋值即可,然后在onBindViewHolder方法中,对状态项处理即可。

下拉刷新

从导语中的图可看出,下拉列表后,列表之上多了一个view,view中有小圆圈等。

下拉列表,列表也往下滑动了。此刻touch事件如何传递呢?touch事件一定没有传递给列表,一定是列表的父控件在处理了,只有这样列表才能下滑,小圆圈才能显示出来。

下拉刷新也有一个技术点需要明确,即如何知晓用户已经将列表滑动到顶部了呢?利用LayoutManager的findFirstVisibleItemPosition接口,即可知晓。

总结上述所提到3点:刷新状态view、touch事件、确定滑动到顶,解决以上3点即可实现此功能。

如图所示,准备一个下拉头view,下拉头view的topmargin值设置成它的负高度,这样正常情况下,下拉头不会显示出来。

然后整体控件重写onTouch事件,如果列表滑动到顶,那么touch事件就自己处理,否则就交给列表。如果是自己处理onTouch事件,则在onTouch中控制下拉头的topmargin即可。

具体代码如下:

public class RefreshableView extends LinearLayout implements View.OnTouchListener {

/**

* 下拉状态

*/

public static final int STATUS_PULL_TO_REFRESH = 0;

/**

* 释放立即刷新状态

*/

public static final int STATUS_RELEASE_TO_REFRESH = 1;

/**

* 正在刷新状态

*/

public static final int STATUS_REFRESHING = 2;

/**

* 刷新完成或未刷新状态

*/

public static final int STATUS_REFRESH_FINISHED = 3;

/**

* 下拉头部回滚的速度

*/

public static final int SCROLL_SPEED = -20;

/**

* 下拉刷新的回调接口

*/

private PullToRefreshListener mListener;

/**

* 下拉头的View

*/

private View header;

/**

* 需要去下拉刷新的ListView

*/

private RecyclerView mRecyclerView;

/**

* 刷新时显示的进度条

*/

private ProgressBar progressBar;

/**

* 指示下拉和释放的文字描述

*/

private TextView description;

/**

* 下拉头的布局参数

*/

private MarginLayoutParams headerLayoutParams;

/**

* 上次更新时间的毫秒值

*/

private long lastUpdateTime;

/**

* 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,使用id来做区分

*/

private int mId = -1;

/**

* 下拉头的高度

*/

private int hideHeaderHeight;

/**

* 当前处理什么状态,可选值有STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH,

* STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED

*/

private int currentStatus = STATUS_REFRESH_FINISHED;;

/**

* 记录上一次的状态是什么,避免进行重复操作

*/

private int lastStatus = currentStatus;

/**

* 手指按下时的屏幕纵坐标

*/

private float yDown;

/**

* 在被判定为滚动之前用户手指可以移动的最大值。

*/

private int touchSlop;

/**

* 是否已加载过一次layout,这里onLayout中的初始化只需加载一次

*/

private boolean loadOnce;

/**

* 当前是否可以下拉,只有ListView滚动到头的时候才允许下拉

*/

private boolean ableToPull;

/**

* 下拉刷新控件的构造函数,会在运行时动态添加一个下拉头的布局。

*

* @param context

* @param attrs

*/

public RefreshableView(Context context, AttributeSet attrs) {

super(context, attrs);

header = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh, null, true);

progressBar = (ProgressBar) header.findViewById(R.id.progress_bar);

description = (TextView) header.findViewById(R.id.description);

touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

setOrientation(VERTICAL);

addView(header, 0);

}

/**

* 进行一些关键性的初始化操作,比如:将下拉头向上偏移进行隐藏,给ListView注册touch事件。

*/

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

super.onLayout(changed, l, t, r, b);

if (changed && !loadOnce) {

hideHeaderHeight = -header.getHeight();

headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();

headerLayoutParams.topMargin = hideHeaderHeight;

header.setLayoutParams(headerLayoutParams);

mRecyclerView = (RecyclerView) getChildAt(1);

mRecyclerView.setOnTouchListener(this);

loadOnce = true;

}

}

/**

* 当ListView被触摸时调用,其中处理了各种下拉刷新的具体逻辑。

*/

@Override

public boolean onTouch(View v, MotionEvent event) {

setIsAbleToPull(event);

if (ableToPull) {

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

yDown = event.getRawY();

break;

case MotionEvent.ACTION_MOVE:

float yMove = event.getRawY();

int distance = (int) (yMove - yDown);

// 如果手指是下滑状态,并且下拉头是完全隐藏的,就屏蔽下拉事件

if (distance <= 0 && headerLayoutParams.topMargin <= hideHeaderHeight) {

return false;

}

if (distance < touchSlop) {

return false;

}

if (currentStatus != STATUS_REFRESHING) {

if (headerLayoutParams.topMargin > 0) {

currentStatus = STATUS_RELEASE_TO_REFRESH;

} else {

currentStatus = STATUS_PULL_TO_REFRESH;

}

// 通过偏移下拉头的topMargin值,来实现下拉效果

headerLayoutParams.topMargin = (distance / 2) + hideHeaderHeight;

header.setLayoutParams(headerLayoutParams);

}

break;

case MotionEvent.ACTION_UP:

default:

if (currentStatus == STATUS_RELEASE_TO_REFRESH) {

// 松手时如果是释放立即刷新状态,就去调用正在刷新的任务

new RefreshingTask().execute();

} else if (currentStatus == STATUS_PULL_TO_REFRESH) {

// 松手时如果是下拉状态,就去调用隐藏下拉头的任务

new HideHeaderTask().execute();

}else if (currentStatus == STATUS_REFRESHING){

float yMove2 = event.getRawY();

int distance2 = (int) (yMove2 - yDown);

if (distance2 < 0 && Math.abs(distance2) > touchSlop){

if (headerLayoutParams.topMargin != hideHeaderHeight) {

headerLayoutParams.topMargin = hideHeaderHeight;

header.setLayoutParams(headerLayoutParams);

currentStatus = STATUS_REFRESH_FINISHED;

}

}

}

break;

}

// 时刻记得更新下拉头中的信息

if (currentStatus == STATUS_PULL_TO_REFRESH

|| currentStatus == STATUS_RELEASE_TO_REFRESH) {

updateHeaderView();

// 当前正处于下拉或释放状态,要让ListView失去焦点,否则被点击的那一项会一直处于选中状态

mRecyclerView.setPressed(false);

mRecyclerView.setFocusable(false);

mRecyclerView.setFocusableInTouchMode(false);

lastStatus = currentStatus;

// 当前正处于下拉或释放状态,通过返回true屏蔽掉ListView的滚动事件

return true;

}

}

return false;

}

/**

* 给下拉刷新控件注册一个监听器。

*

* @param listener

* 监听器的实现。

* @param id

* 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突, 请不同界面在注册下拉刷新监听器时一定要传入不同的id。

*/

public void setOnRefreshListener(PullToRefreshListener listener, int id) {

mListener = listener;

mId = id;

}

/**

* 当所有的刷新逻辑完成后,记录调用一下,否则你的ListView将一直处于正在刷新状态。

*/

public void finishRefreshing() {

currentStatus = STATUS_REFRESH_FINISHED;

new HideHeaderTask().execute();

}

/**

* 根据当前ListView的滚动状态来设定 {@link #ableToPull}

* 的值,每次都需要在onTouch中第一个执行,这样可以判断出当前应该是滚动ListView,还是应该进行下拉。

*

* @param event

*/

private void setIsAbleToPull(MotionEvent event) {

View firstChild = mRecyclerView.getChildAt(0);

if (firstChild != null) {

LinearLayoutManager manager = (LinearLayoutManager) mRecyclerView.getLayoutManager();

int firstVisiblePos = manager.findFirstVisibleItemPosition();

RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) firstChild.getLayoutParams();

if (firstVisiblePos == 0 && firstChild.getTop() == params.topMargin) {

if (!ableToPull) {

yDown = event.getRawY();

}

// 如果首个元素的上边缘,距离父布局值为0,就说明ListView滚动到了最顶部,此时应该允许下拉刷新

ableToPull = true;

} else {

if (headerLayoutParams.topMargin != hideHeaderHeight) {

headerLayoutParams.topMargin = hideHeaderHeight;

header.setLayoutParams(headerLayoutParams);

currentStatus = STATUS_REFRESH_FINISHED;

}

ableToPull = false;

}

} else {

// 如果ListView中没有元素,也应该允许下拉刷新

ableToPull = true;

}

}

/**

* 更新下拉头中的信息。

*/

private void updateHeaderView() {

if (lastStatus != currentStatus) {

if (currentStatus == STATUS_PULL_TO_REFRESH) {

description.setText(getResources().getString(R.string.pull_to_refresh));

progressBar.setVisibility(View.GONE);

} else if (currentStatus == STATUS_RELEASE_TO_REFRESH) {

description.setText(getResources().getString(R.string.pull_to_refresh));

progressBar.setVisibility(View.GONE);

} else if (currentStatus == STATUS_REFRESHING) {

description.setText(getResources().getString(R.string.refreshing));

progressBar.setVisibility(View.VISIBLE);

}

}

}

/**

* 正在刷新的任务,在此任务中会去回调注册进来的下拉刷新监听器。

*/

class RefreshingTask extends AsyncTask {

@Override

protected Void doInBackground(Void... params) {

int topMargin = headerLayoutParams.topMargin;

while (true) {

topMargin = topMargin + SCROLL_SPEED;

if (topMargin <= 0) {

topMargin = 0;

break;

}

publishProgress(topMargin);

// sleep(10);

}

currentStatus = STATUS_REFRESHING;

publishProgress(0);

if (mListener != null) {

mListener.onRefresh();

}

return null;

}

@Override

protected void onProgressUpdate(Integer... topMargin) {

updateHeaderView();

headerLayoutParams.topMargin = topMargin[0];

header.setLayoutParams(headerLayoutParams);

}

}

/**

* 隐藏下拉头的任务,当未进行下拉刷新或下拉刷新完成后,此任务将会使下拉头重新隐藏。

*

*/

class HideHeaderTask extends AsyncTask {

@Override

protected Integer doInBackground(Void... params) {

int topMargin = headerLayoutParams.topMargin;

while (true) {

topMargin = topMargin + SCROLL_SPEED;

if (topMargin <= hideHeaderHeight) {

topMargin = hideHeaderHeight;

break;

}

publishProgress(topMargin);

// sleep(10);

}

return topMargin;

}

@Override

protected void onProgressUpdate(Integer... topMargin) {

headerLayoutParams.topMargin = topMargin[0];

header.setLayoutParams(headerLayoutParams);

}

@Override

protected void onPostExecute(Integer topMargin) {

headerLayoutParams.topMargin = topMargin;

header.setLayoutParams(headerLayoutParams);

currentStatus = STATUS_REFRESH_FINISHED;

}

}

/**

* 使当前线程睡眠指定的毫秒数。

*

* @param time

* 指定当前线程睡眠多久,以毫秒为单位

*/

private void sleep(int time) {

try {

Thread.sleep(time);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

/**

* 下拉刷新的监听器,使用下拉刷新的地方应该注册此监听器来获取刷新回调。

*/

public interface PullToRefreshListener {

/**

* 刷新时会去回调此方法,在方法内编写具体的刷新逻辑。注意此方法是在子线程中调用的, 你可以不必另开线程来进行耗时操作。

*/

void onRefresh();

}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值