本文主要内容

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

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

说说分页加载及下拉刷新

下拉改写,假如用户对当时界面数据不感兴趣,需求检查其它数据,能够下拉改写,从服务器从头获取数据。类似新闻类的app内都有这种功用。

说说分页加载及下拉刷新

分页加载

假定当时页面高度只够显现3个数据项,服务器上却有4个数据项,假如运用分页加载会是什么样呢?

说说分页加载及下拉刷新

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

说说分页加载及下拉刷新

榜首、二项数据会被躲藏掉,界面将显现第三项数据,第四项数据以及加载中的状况。当然,假如咱们检测到服务器没有更多数据了,那咱们还能够将加载中的状况改成已最新状况。

假定服务器的数据有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,将positionRecycleView显现的总数据数量一比,假如一致,那么用户就将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事情必定没有传递给列表,必定是列表的父控件在处理了,只要这样列表才干下滑,小圆圈才干显现出来。

下拉改写也有一个技能点需求清晰,即怎么知晓用户现已将列表滑动到顶部了呢?运用LayoutManagerfindFirstVisibleItemPosition接口,即可知晓。

总结上述所说到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<Void, Integer, Void> {
    @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<Void, Integer, Integer> {
    @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();
}
}