RecyclerView 上拉加载 PullToRefreshRecyclerView
来源:互联网 发布:放置江湖人物评价算法 编辑:程序博客网 时间:2024/05/20 17:27
## 设计思路ListView 上拉加载很容易实现,监听 ListView 滑动到底部,显示 FootView 即可,但是 RecyclerView 没有 addFootView(View view) 这个方法,有一种方案是在 adapter 中加一个 itemView 用于显示 FootView,但是这样做就得每一个使用到 RecyclerView 的地方都写一遍重复代码,而且不能适配多种类型的 LayoutManager,所以 RecyclerView 就需要另外一种思路来实现,我的解决方案是:自定义 PullToRefreshRecyclerView 继承 FrameLayout,将 RecyclerView 与 FootView 添加到 PullToRefreshRecyclerView 中,上拉时显示 FootView。效果如下:## 具体方案布局文件中很简单,一个 FrameLayout 内部放一个 RecyclerView 和 一个 FootView,布局文件如下:
<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:id="@+id/root_view" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="wrap_content"/> <LinearLayout android:id="@+id/footer_view" android:layout_width="match_parent" android:layout_height="80dp" android:orientation="horizontal" android:gravity="center" android:layout_gravity="bottom"> <ProgressBar android:id="@+id/progress" android:layout_width="20dp" android:layout_height="20dp" android:visibility="gone" android:layout_marginRight="5dp"/> <ImageView android:id="@+id/img_arrow" android:layout_width="20dp" android:layout_height="20dp" android:scaleType="centerInside" android:src="@mipmap/arrow"/> <TextView android:id="@+id/tv_load_tag" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:layout_marginLeft="5dp" android:text="上拉加载数据"/> </LinearLayout></FrameLayout>
这个布局的效果如下图:可以看到有个很明显的问题,FootView 是一直在屏幕内部的,即使隐藏掉滑动到底部在显示效果也很差劲,我的解决方案是重写 PullToRefreshRecyclerView 的 onMeasure 方法,将 PullToRefreshRecyclerView 高度设置为屏幕高度加上 FootView 的高度,上拉时使用 scrollTo 方法显示 FootView。
onMeasure 方法如下:
@Overridepublic void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int height = getMeasuredHeight() + footViewHeight; setMeasuredDimension(getMeasuredWidth(), height); LayoutParams rootLP = (LayoutParams) rootView.getLayoutParams(); rootLP.height = height; rootView.setLayoutParams(rootLP);//设置 FootView的高度 LayoutParams recyclerLp = (LayoutParams) recyclerView.getLayoutParams(); recyclerLp.height = height - footViewHeight; recyclerView.setLayoutParams(recyclerLp);//将 RecyclerView 的高度设置为屏幕高度}
这样基本布局就完成了,然后就是对滑动事件的拦截与分发,如果滑动到底部则拦截事件,并且根据滑动距离来计算 scrollTo 的移动距离,具体代码如下:
@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) { if (recyclerView == null || recyclerView.getChildCount() == 0) return super.onInterceptTouchEvent(ev); if(isLoading) return true;//如果正在加载中则拦截滑动事件 switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: lastDownY = (int) ev.getY(); break; case MotionEvent.ACTION_MOVE: int lastPosition = -1; //以下代码用于获取当前 RecyclerView 中显示的最后一个 position RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof GridLayoutManager) { lastPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); } else if (layoutManager instanceof LinearLayoutManager) { lastPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); } else if (layoutManager instanceof StaggeredGridLayoutManager) { int[] lastPositions = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()]; ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(lastPositions); lastPosition = findMax(lastPositions); } int offerY = (int) ev.getY() - lastDownY; if (offerY < 0) { //如果正在上拉 View lastView = recyclerView.getChildAt(recyclerView.getChildCount() - 1); if (lastView != null && lastView.getBottom() + footViewHeight >= getHeight() && lastPosition == recyclerView.getLayoutManager().getItemCount() - 1) { //如果滑动到最底部则拦截事件并设置标志位 canScroll = true; return true; } else { canScroll = false; } } else { canScroll = false; } break; } return super.onInterceptTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent ev) { if (recyclerView == null || recyclerView.getChildCount() == 0) return super.onInterceptTouchEvent(ev); int offerY; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: lastDownY = (int) ev.getY(); break; case MotionEvent.ACTION_MOVE: if (canScroll) { offerY = (int) ev.getY() - lastDownY; lastOfferY = offerY; if(footView.getVisibility() == GONE) footView.setVisibility(VISIBLE); //将 PullToRefreshRecyclerView 内部跟着手指向上移动,移动距离为手指滑动距离的一半 scrollTo(getScrollX(), -offerY / 2); imgArrow.setVisibility(VISIBLE); if (Math.abs(offerY) / 2 < footViewHeight) { progressBar.setVisibility(GONE); tvLoadTag.setText("上拉加载数据"); if (!arrowIsTop) { imgArrow.startAnimation(topAnimation); arrowIsTop = true; } canLoad = false; } else { progressBar.setVisibility(GONE); tvLoadTag.setText("松手加载更多"); if (arrowIsTop) { imgArrow.startAnimation(bottomAnimation); arrowIsTop = false; } canLoad = true; } } break; case MotionEvent.ACTION_UP: if (canScroll) { if (!canLoad) { //手指松开后如果滑动距离小于设定距离则回到初始状态 scrollTo(getScrollX(), 0); } else { //如果滑动距离大于设定距离则加载数据并回弹到加载状态 mScroller.startScroll(getScrollX(), getScrollY(), getScrollX(), -(Math.abs(lastOfferY) / 2 - footViewHeight), 500); lastOfferY = 0; loadData(); } canScroll = false; } break; } return super.onTouchEvent(ev);}
以上便是事件的拦截与分发,另外需要注意的是:如果与 SwipeRefreshLayout 结合使用会出现一个小问题,就是当处于上拉加载状态时如果下拉可能会出现滑动冲突,当然了也有解决方案,自定义的这个 PullToRefreshRecyclerView 类实现了 SwipeRefreshLayout.OnChildScrollUpCallback 接口,当 SwipeRefreshLayout 判断当前 View 是否处于可下拉刷新状态时会首先使用这个接口来判断,我这里做了相应的方法,使用时只需要将 PullToRefreshRecyclerView 对象传给 SwipeRefreshLayout.OnChildScrollUpCallback 接口即可,如下:
swipeRefresh.setOnChildScrollUpCallback(pullToRefreshRecyclerView);
下面放上 PullToRefreshRecyclerView 源码:
package com.zhangke.widget;import android.content.Context;import android.support.annotation.Nullable;import android.support.v4.view.ViewPager;import android.support.v4.widget.SwipeRefreshLayout;import android.support.v7.widget.GridLayoutManager;import android.support.v7.widget.LinearLayoutManager;import android.support.v7.widget.RecyclerView;import android.support.v7.widget.StaggeredGridLayoutManager;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.view.ViewParent;import android.view.animation.Animation;import android.view.animation.RotateAnimation;import android.widget.FrameLayout;import android.widget.ImageView;import android.widget.ProgressBar;import android.widget.Scroller;import android.widget.TextView;import com.seagetech.ptduser.test.R;/** * Created by 张可 on 2017/7/5. */public class PullToRefreshRecyclerView extends FrameLayout implements SwipeRefreshLayout.OnChildScrollUpCallback { private static final String TAG = "PullToRefreshRecycler"; private View rootView; private RecyclerView recyclerView; private View footView; private ProgressBar progressBar; private TextView tvLoadTag; private ImageView imgArrow; private int footViewHeight = 100; private int lastDownY; private boolean canScroll = false; private boolean canLoad = false; private boolean isLoading = false;//是否正在加载,正在加载时拦截滑动事件 /** * 箭头方向是否向上 */ private boolean arrowIsTop = true; private RotateAnimation bottomAnimation;//箭头由上到下的动画 private RotateAnimation topAnimation;//箭头由下到上的动画 private Scroller mScroller; private OnPullToBottomListener onPullToBottomListener; private int lastOfferY = 0; public PullToRefreshRecyclerView(Context context) { super(context); init(); } public PullToRefreshRecyclerView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } private void init() { inflate(getContext(), R.layout.view_pull_to_refresh_recycler, this); rootView = findViewById(R.id.root_view); recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view); footView = findViewById(R.id.footer_view); progressBar = (ProgressBar) findViewById(R.id.progress); tvLoadTag = (TextView) findViewById(R.id.tv_load_tag); imgArrow = (ImageView) findViewById(R.id.img_arrow); footViewHeight = dip2px(getContext(), 80); bottomAnimation = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); bottomAnimation.setDuration(200); bottomAnimation.setFillAfter(true); topAnimation = new RotateAnimation(180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); topAnimation.setDuration(200); topAnimation.setFillAfter(true); mScroller = new Scroller(getContext()); } public void setLayoutManager(RecyclerView.LayoutManager layout) { recyclerView.setLayoutManager(layout); } public void setAdapter(RecyclerView.Adapter adapter) { recyclerView.setAdapter(adapter); adapter.notifyDataSetChanged(); } public RecyclerView getRecyclerView() { return recyclerView; } @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int height = getMeasuredHeight() + footViewHeight; setMeasuredDimension(getMeasuredWidth(), height); LayoutParams rootLP = (LayoutParams) rootView.getLayoutParams(); rootLP.height = height; rootView.setLayoutParams(rootLP); LayoutParams recyclerLp = (LayoutParams) recyclerView.getLayoutParams(); recyclerLp.height = height - footViewHeight; recyclerView.setLayoutParams(recyclerLp); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (recyclerView == null || recyclerView.getChildCount() == 0) return super.onInterceptTouchEvent(ev); if(isLoading) return true; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: lastDownY = (int) ev.getY(); break; case MotionEvent.ACTION_MOVE: int lastPosition = -1; RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof GridLayoutManager) { lastPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); } else if (layoutManager instanceof LinearLayoutManager) { lastPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); } else if (layoutManager instanceof StaggeredGridLayoutManager) { int[] lastPositions = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()]; ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(lastPositions); lastPosition = findMax(lastPositions); } int offerY = (int) ev.getY() - lastDownY; if (offerY < 0) { View lastView = recyclerView.getChildAt(recyclerView.getChildCount() - 1); if (lastView != null && lastView.getBottom() + footViewHeight >= getHeight() && lastPosition == recyclerView.getLayoutManager().getItemCount() - 1) { canScroll = true; return true; } else { canScroll = false; } } else { canScroll = false; } break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { if (recyclerView == null || recyclerView.getChildCount() == 0) return super.onInterceptTouchEvent(ev); int offerY; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: lastDownY = (int) ev.getY(); break; case MotionEvent.ACTION_MOVE: if (canScroll) { offerY = (int) ev.getY() - lastDownY; lastOfferY = offerY; if(footView.getVisibility() == GONE) footView.setVisibility(VISIBLE); scrollTo(getScrollX(), -offerY / 2); imgArrow.setVisibility(VISIBLE); if (Math.abs(offerY) / 2 < footViewHeight) { progressBar.setVisibility(GONE); tvLoadTag.setText("上拉加载数据"); if (!arrowIsTop) { imgArrow.startAnimation(topAnimation); arrowIsTop = true; } canLoad = false; } else { progressBar.setVisibility(GONE); tvLoadTag.setText("松手加载更多"); if (arrowIsTop) { imgArrow.startAnimation(bottomAnimation); arrowIsTop = false; } canLoad = true; } } break; case MotionEvent.ACTION_UP: if (canScroll) { if (!canLoad) { scrollTo(getScrollX(), 0); } else { mScroller.startScroll(getScrollX(), getScrollY(), getScrollX(), -(Math.abs(lastOfferY) / 2 - footViewHeight), 500); lastOfferY = 0; loadData(); } canScroll = false; } break; } return super.onTouchEvent(ev); } @Override public boolean canChildScrollUp(SwipeRefreshLayout parent, @Nullable View child) { if(recyclerView.getChildCount() > 0 && recyclerView.getChildAt(0).getTop() < recyclerView.getPaddingTop()){ Log.e(TAG, "canChildScrollUp return true"); return true; } Log.e(TAG, "canChildScrollUp return false"); return false; } private void loadData() { isLoading = true; imgArrow.clearAnimation(); imgArrow.setVisibility(GONE); progressBar.setVisibility(VISIBLE); tvLoadTag.setText("正在加载..."); if (this.onPullToBottomListener != null) { postDelayed(new Runnable() { @Override public void run() { PullToRefreshRecyclerView.this.onPullToBottomListener.onPullToBottom(); } }, 500); } else { progressBar.setVisibility(GONE); scrollTo(getScrollX(), 0); } } /** * 设置是否正在加载,一般来书,在加载完毕之后应该调用此方法 * * @param loading */ public void setLoading(final boolean loading) { isLoading = loading; if (!isLoading) { post(new Runnable() { @Override public void run() { if (!loading) { progressBar.setVisibility(GONE); scrollTo(getScrollX(), 0); } } }); } } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } } //找到数组中的最大值 private int findMax(int[] lastPositions) { int max = lastPositions[0]; for (int value : lastPositions) { if (value > max) { max = value; } } return max; } public void setOnPullToBottomListener(OnPullToBottomListener onPullToBottomListener) { this.onPullToBottomListener = onPullToBottomListener; } public interface OnPullToBottomListener { void onPullToBottom(); } /** * 将dip或dp值转换为px值,保证尺寸大小不变 * * @param dipValue * @param dipValue (DisplayMetrics类中属性density) * @return */ public static int dip2px(Context context, float dipValue) { final float scale = context.getResources().getDisplayMetrics().density; return (int) (dipValue * scale + 0.5f); }}
点次查看源码及资源文件
阅读全文
0 0
- RecyclerView 上拉加载 PullToRefreshRecyclerView
- PullToRefreshRecyclerView——带上拉刷新下拉加载功能的RecyclerView
- RecyclerView上拉加载
- RecyclerView 上拉加载更多
- android RecyclerView上拉加载
- RecyclerView上拉加载Demo
- RecyclerView的上拉加载
- RecyclerView上拉加载更多数据
- RecyclerView实现上拉加载,下拉刷新
- RecyclerView 下拉刷新上拉加载更多
- 自定义RecyclerView实现上拉加载
- RecyclerView实现上拉加载,下拉刷新
- RecyclerView 下拉刷新上拉加载更多
- RecyclerView的上拉加载,下拉刷新
- RecyclerView 下拉刷新和上拉加载
- RecyclerView下拉刷新上拉加载
- RecyclerView下拉刷新上拉加载
- RecyclerView下拉刷新上拉加载
- 面向对象——继承
- 汇编语言中对PSP区和程序区的一些理解
- 消息队列 初探
- MyBatis TypeHandler
- 计算机数值方法
- RecyclerView 上拉加载 PullToRefreshRecyclerView
- 分布式跟踪系统(一):Zipkin的背景和设计
- Markdown编辑器快捷键使用
- 三向单词查找树(c++版)
- 分布式跟踪系统(二):Zipkin的Span模型
- 《HBase权威指南》读书笔记 第八章:架构,WAL预写日志
- webstrom下运行gulp初试
- bzoj5045: 打砖块 (粗制滥造)
- jQuery操作表格(table)的常用方法、技巧汇总