通过导入第三方库实现ListView的上拉加载和下拉刷新比较简单,今天我要讲的是自定义RecyclerView实现下拉刷新和上拉加载。首先,自定义下拉刷新上拉加载是通过给RecyclerView添加头部和尾部实现的。而问题是RecyclerView并没有addHeaderView(View v)和addFooterView(View v)方法。
第一步:自定义HeaderAndFooterWrapper(装饰者模式)实现给RecyclerView添加头部和尾部
方法:public void addHeaderView(View v)
public void addFooterView(View v)
代码参照张鸿洋:Android 优雅的为RecyclerView添加HeaderView和FooterView
链接:http://blog.csdn.net/lmj623565791/article/details/51854533
第二步:定义类RefreshRecyclerView并抽取为库
1) 头部布局:refresh_recyclerview_header.xml
-
<?xml version="1.0" encoding="utf-8"?>
-
<!--下拉刷新控件-->
-
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
android:layout_width="match_parent"
-
android:layout_height="wrap_content"
-
android:gravity="center"
-
android:orientation="horizontal">
-
<FrameLayout
-
android:layout_width="wrap_content"
-
android:layout_height="wrap_content"
-
android:padding="10dp">
-
<ImageView
-
android:id="@+id/iv_header_refresh"
-
android:layout_width="wrap_content"
-
android:layout_height="wrap_content"
-
android:layout_gravity="center"
-
android:src="@drawable/headview_red_arrow" />
-
<ProgressBar
-
android:id="@+id/pb_header_refresh"
-
android:layout_width="wrap_content"
-
android:layout_height="wrap_content"
-
android:layout_gravity="center"
-
android:indeterminateDrawable="@drawable/custom_progressbar"
-
android:visibility="gone" />
-
</FrameLayout>
-
<LinearLayout
-
android:layout_width="wrap_content"
-
android:layout_height="wrap_content"
-
android:layout_gravity="center_vertical"
-
android:orientation="vertical">
-
<TextView
-
android:id="@+id/tv_status"
-
android:layout_width="match_parent"
-
android:layout_height="wrap_content"
-
android:gravity="center_horizontal"
-
android:text="下拉刷新"
-
android:textColor="#ff0000"
-
android:textSize="18sp" />
-
<TextView
-
android:id="@+id/tv_time"
-
android:layout_width="match_parent"
-
android:layout_height="wrap_content"
-
android:layout_marginTop="5dp"
-
android:gravity="center_horizontal"
-
android:text="上次更新时间:2016-10-31"
-
android:textColor="#55000000"
-
android:textSize="16sp" />
-
</LinearLayout>
-
</LinearLayout>
2)尾部布局(上拉加载部分):refresh_recyclerview_footer.xml
-
<span style="font-size:14px;"><?xml version="1.0" encoding="utf-8"?>
-
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
android:layout_width="match_parent"
-
android:gravity="center"
-
android:layout_height="wrap_content"
-
android:orientation="horizontal">
-
<ProgressBar
-
android:indeterminateDrawable="@drawable/custom_progressbar"
-
android:layout_margin="5dp"
-
android:layout_width="wrap_content"
-
android:layout_height="wrap_content" />
-
<TextView
-
android:text="加载更多中...."
-
android:textColor="#ff0000"
-
android:textSize="25sp"
-
android:layout_marginLeft="10dp"
-
android:layout_width="wrap_content"
-
android:layout_height="wrap_content" />
-
</LinearLayout></span>
3)自定义ProgressBar样式custom_progressbar.xml
-
<span style="font-size:14px;"><?xml version="1.0" encoding="utf-8"?>
-
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
-
android:fromDegrees="0"
-
android:toDegrees="360"
-
android:pivotX="50%"
-
android:pivotY="50%">
-
<shape android:shape="ring"
-
android:innerRadiusRatio="2.5"
-
android:thicknessRatio="15"
-
android:useLevel="false"
-
>
-
<gradient android:startColor="#ff0000"
-
android:endColor="#ffffff"
-
android:centerColor="#88ff0000"
-
android:type="sweep"></gradient>
-
</shape>
-
</rotate></span>
代码如下:
-
public class RefreshRecyclerView extends RecyclerView {
-
private final Context mContext;
-
// 顶部视图,下拉刷新控件
-
private LinearLayout headerView;
-
// 正在刷新状态的进度条
-
private ProgressBar pb_header_refresh;
-
// 刷新箭头
-
private ImageView iv_header_refresh;
-
// 显示刷新状态
-
private TextView tv_status;
-
// 显示最近一次的刷新时间
-
private TextView tv_time;
-
// 转到下拉刷新状态时的动画
-
private RotateAnimation downAnima;
-
// 转到释放刷新状态时的动画
-
private RotateAnimation upAnima;
-
//触摸事件中按下的Y坐标,初始值为-1,为防止ACTION_DOWN事件被抢占
-
private float startY = -1;
-
// 下拉刷新控件的高度
-
private int pulldownHeight;
-
// 刷新状态:下拉刷新
-
private final int PULL_DOWN_REFRESH = 0;
-
// 刷新状态:释放刷新
-
private final int RELEASE_REFRESH = 1;
-
// 刷新状态:正常刷新
-
private final int REFRESHING = 2;
-
// 当前头布局的状态-默认为下拉刷新
-
private int currState = PULL_DOWN_REFRESH;
-
//
-
private RefreshRecyclerView.OnRefreshListener mOnRefreshListener;
-
// 尾部视图
-
private View footerView;
-
// 尾部试图(上拉加载控件)的高度
-
private int footerViewHeight;
-
// 判断是否是加载更多
-
private boolean isLoadingMore;
-
public RefreshRecyclerView(Context context) {
-
this(context, null);
-
}
-
public RefreshRecyclerView(Context context, AttributeSet attrs) {
-
this(context, attrs, 0);
-
}
-
public RefreshRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
-
super(context, attrs, defStyleAttr);
-
mContext = context;
-
initHeaderView();
-
initFooterView();
-
}
-
/**
-
* 返回尾部布局,供外部调用
-
* @return
-
*/
-
public View getFooterView(){
-
return footerView;
-
}
-
/**
-
* 返回头部布局,供外部调用
-
* @return
-
*/
-
public View getHeaderView(){
-
return headerView;
-
}
-
/**
-
* 通过HeaderAndFooterWrapper对象给RecyclerView添加尾部
-
* @param footerView 尾部视图
-
* @param headerAndFooterWrapper RecyclerView.Adapter的包装类对象,通过它给RecyclerView添加尾部视图
-
*/
-
public void addFooterView(View footerView, HeaderAndFooterWrapper headerAndFooterWrapper) {
-
headerAndFooterWrapper.addFooterView(footerView);
-
}
-
/**
-
* 通过HeaderAndFooterWrapper对象给RecyclerView添加头部部
-
* @param headerView 尾部视图
-
* @param headerAndFooterWrapper RecyclerView.Adapter的包装类对象,通过它给RecyclerView添加头部视图
-
*/
-
public void addHeaderView(View headerView,HeaderAndFooterWrapper headerAndFooterWrapper) {
-
headerAndFooterWrapper.addHeaderView(headerView);
-
}
初始化头部布局和尾部布局
-
private void initHeaderView() {
-
headerView = (LinearLayout) View.inflate(mContext, R.layout.refresh_recyclerview_header, null);
-
tv_time = (TextView) headerView.findViewById(R.id.tv_time);
-
tv_status = (TextView) headerView.findViewById(R.id.tv_status);
-
iv_header_refresh = (ImageView) headerView.findViewById(R.id.iv_header_refresh);
-
pb_header_refresh = (ProgressBar) headerView.findViewById(R.id.pb_header_refresh);
-
headerView.measure(0, 0);
-
pulldownHeight = headerView.getMeasuredHeight();
-
headerView.setPadding(0, -pulldownHeight, 0, 0);
-
//初始化头部布局的动画
-
initAnimation();
-
}
-
/**
-
* 刷新状态改变时的动画
-
*/
-
private void initAnimation() {
-
// 从下拉刷新状态转换为释放刷新状态
-
upAnima = new RotateAnimation(0, -180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
-
upAnima.setFillAfter(true);
-
upAnima.setDuration(500);
-
// 转到下拉刷新的动画
-
downAnima = new RotateAnimation(-180, -360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
-
downAnima.setFillAfter(true);
-
downAnima.setDuration(500);
-
}
-
private void initFooterView() {
-
footerView = View.inflate(mContext, R.layout.refresh_recyclerview_footer, null);
-
footerView.measure(0, 0);
-
//得到控件的高
-
footerViewHeight = footerView.getMeasuredHeight();
-
//默认隐藏下拉刷新控件
-
// View.setPadding(0,-控件高,0,0);//完全隐藏
-
//View.setPadding(0, 0,0,0);//完全显示
-
footerView.setPadding(0, -footerViewHeight, 0, 0);
-
// addFooterView(footerView);
-
// 自己监听自己
-
this.addOnScrollListener(new MyOnScrollListener());
-
}
重写onTouchEvent()方法,startY表示按下时的坐标,但是有可能ACTION_DOWN事件被强占,那么case MotionEvent.ACTION_DOWN:就没机会执行。所以case MotionEvent.ACTION_MOVE:子句时首先判断startY是否为-1,是的话把第一次移动的坐标当作按下时的坐
标。然后判断当前的状态,如果正在刷新就不必再次执行下面的刷新代码,直接跳出。dY>0表示用户正在执行下拉操作,因为头部视图初始坐标为-pulldownHeight,所以手指在屏幕上滑动多少距离,顶部视图的y坐标就应该增加相应值。paddingTop==0是一个状态
分界线,paddingTop < 0 是下拉刷新状态,大于0是释放刷新状态。根据paddingTop的值跳转相应的状态。最后在手指抬起的时候,首先重置startY = -1;然后判断刷新状态,如果是下拉刷新(头部视图没有完全显示)就设置下拉控件为默认隐藏状态,如果是释放
刷新状态(paddingTop > 0),就跳转到正在刷新状态,并让下拉控件完全显示同时调用用户的回调事件,刷新页面数据。
-
@Override
-
public boolean onTouchEvent(MotionEvent ev) {
-
switch (ev.getAction()) {
-
case MotionEvent.ACTION_DOWN:
-
startY = ev.getY();
-
break;
-
case MotionEvent.ACTION_MOVE:
-
//防止ACTION_DOWN事件被抢占,没有执行
-
if (startY == -1) {
-
startY = ev.getY();
-
}
-
float endY = ev.getY();
-
float dY = endY - startY;
-
//判断当前是否正在刷新中
-
if (currState == REFRESHING) {
-
//如果当前是正在刷新,不执行下拉刷新了,直接break;
-
break;
-
}
-
// 如果是下拉
-
if (dY > 0) {
-
int paddingTop = (int) (dY - pulldownHeight);
-
if (paddingTop > 0 && currState != RELEASE_REFRESH) {
-
//完全显示下拉刷新控件,进入松开刷新状态
-
currState = RELEASE_REFRESH;
-
refreshViewState();
-
} else if (paddingTop < 0 && currState != PULL_DOWN_REFRESH) {
-
//没有完全显示下拉刷新控件,进入下拉刷新状态
-
currState = PULL_DOWN_REFRESH;
-
refreshViewState();
-
}
-
headerView.setPadding(0, paddingTop, 0, 0);
-
}
-
break;
-
case MotionEvent.ACTION_UP:
-
//5.重新记录值
-
startY = -1;
-
if (currState == PULL_DOWN_REFRESH) {
-
//设置默认隐藏
-
headerView.setPadding(0, -pulldownHeight, 0, 0);
-
} else if (currState == RELEASE_REFRESH) {
-
//当前是释放刷新,进入到正在刷新状态,完全显示
-
currState = REFRESHING;
-
refreshViewState();
-
headerView.setPadding(0, 0, 0, 0);
-
//调用用户的回调事件,刷新页面数据
-
if (mOnRefreshListener != null) {
-
mOnRefreshListener.onPullDownRefresh();
-
}
-
}
-
break;
-
}
-
return super.onTouchEvent(ev);
-
}
-
/**
-
* 跳转刷新状态
-
*/
-
private void refreshViewState() {
-
switch (currState) {
-
// 跳转到下拉刷新
-
case PULL_DOWN_REFRESH:
-
iv_header_refresh.startAnimation(downAnima);
-
tv_status.setText("下拉刷新");
-
break;
-
// 跳转到释放刷新
-
case RELEASE_REFRESH:
-
iv_header_refresh.startAnimation(upAnima);
-
tv_status.setText("释放刷新");
-
break;
-
// 跳转到正在刷新
-
case REFRESHING:
-
iv_header_refresh.clearAnimation();
-
iv_header_refresh.setVisibility(GONE);
-
pb_header_refresh.setVisibility(VISIBLE);
-
tv_status.setText("正在刷新中.....");
-
break;
-
}
-
}
定义接口
-
/**
-
* 定义下拉刷新和上拉加载的接口
-
*/
-
public interface OnRefreshListener {
-
/**
-
* 当下拉刷新时触发此方法
-
*/
-
void onPullDownRefresh();
-
/**
-
* 当加载更多的时候回调这个方法
-
*/
-
void onLoadingMore();
-
}
-
public void setOnRefreshListener(RefreshRecyclerView.OnRefreshListener listener) {
-
this.mOnRefreshListener = listener;
-
}
-
/**
-
* 当刷新完数据之后,调用此方法,把头文件隐藏,并且状态设置为初始状态
-
* @param isSuccess
-
*/
-
public void onFinishRefresh(boolean isSuccess) {
-
if (isLoadingMore) {
-
footerView.setPadding(0, -footerViewHeight, 0, 0);
-
isLoadingMore = false;
-
} else {
-
headerView.setPadding(0, -pulldownHeight, 0, 0);
-
currState = PULL_DOWN_REFRESH;
-
iv_header_refresh.setVisibility(VISIBLE);
-
pb_header_refresh.setVisibility(GONE);
-
tv_status.setText("下拉刷新");
-
if (isSuccess) {
-
//设置更新时间
-
tv_time.setText(getSystemTime());
-
}
-
}
-
}
-
private String getSystemTime() {
-
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-
return dateFormat.format(new Date());
-
}
上拉加载逻辑的实现,重写onScrollStateChanged()方法。如果newState为空闲状态或者是快速滑动状态就判断RecyclerView的最后一个可见Item的索引是否 >= getChildCount()-1。是的话就触发加载更多事件,完全显示上拉加载控件并调用回调事件。这里要注意:ListView是SCROLL_STATE_FLING而RecyclerView是SCROLL_STATE_SETTLING,ListView得到最后一个可见Item索引直接
getLastVisiblePosition()方法就行了。RecyclerView比较麻烦:首先要判断布局管理器是否是线性布局管理器,因为只有LinearLayoutManager才有查找第一个和最后一个可见View位置的方法。
-
private class MyOnScrollListener extends OnScrollListener {
-
@Override
-
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
-
// super.onScrollStateChanged(recyclerView, newState);
-
if (newState == SCROLL_STATE_IDLE || newState == SCROLL_STATE_SETTLING) {
-
//判断是当前layoutManager是否为LinearLayoutManager
-
// 只有LinearLayoutManager才有查找第一个和最后一个可见view位置的方法
-
LayoutManager layoutManager = recyclerView.getLayoutManager();
-
if (layoutManager instanceof LinearLayoutManager) {
-
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
-
//当停止滚动时或者惯性滚动时,RecyclerView的最后一个显示的条目:getCount()-1
-
// 注意是findLastVisibleItemPosition()而不是getLastVisiblePosition
-
if (linearLayoutManager.findLastVisibleItemPosition() >= getChildCount() - 1) {
-
isLoadingMore = true;
-
//把底部加载显示
-
footerView.setPadding(0, 0, 0, 0);
-
if (mOnRefreshListener != null) {
-
mOnRefreshListener.onLoadingMore();
-
}
-
}
-
}
-
}
-
}
-
}
第三步:使用 自定义的RecyclerView
首先上布局
-
<?xml version="1.0" encoding="utf-8"?>
-
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
android:layout_width="match_parent"
-
android:layout_height="match_parent"
-
android:orientation="vertical">
-
<com.example.refreshrecyclerview_library.RefreshRecyclerView
-
android:id="@+id/custom_recyclerview"
-
android:layout_width="match_parent"
-
android:layout_height="0dp"
-
android:layout_weight="1"
-
android:cacheColorHint="@android:color/transparent"
-
android:divider="@null"
-
android:fadingEdge="none" />
-
</LinearLayout>
测试代码
-
public class MainActivity extends Activity {
-
private static final int REFRESH = 0;
-
private static final int LOADMORE = 1;
-
private HeaderAndFooterWrapper headerAndFooterWrapper;
-
private RefreshRecyclerViewAdapter recyclerAdapter;
-
private RefreshRecyclerView custom_recyclerview;
-
private Handler handler = new Handler(){
-
public void handleMessage(Message msg){
-
switch (msg.what) {
-
case REFRESH:
-
custom_recyclerview.onFinishRefresh(true);
-
break;
-
case LOADMORE:
-
custom_recyclerview.onFinishRefresh(false);
-
break;
-
}
-
}
-
};
-
@Override
-
protected void onCreate(Bundle savedInstanceState) {
-
super.onCreate(savedInstanceState);
-
setContentView(R.layout.activity_main);
-
custom_recyclerview = (RefreshRecyclerView) findViewById(R.id.custom_recyclerview);
-
initRefreshRecyclerView();
-
}
-
private void initRefreshRecyclerView() {
-
// 给Recycler设置分割线
-
custom_recyclerview.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
-
recyclerAdapter = new RefreshRecyclerViewAdapter(this);
-
headerAndFooterWrapper = new HeaderAndFooterWrapper(recyclerAdapter);
-
// 不要忘记设置布局管理器
-
custom_recyclerview.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
-
custom_recyclerview.setAdapter(headerAndFooterWrapper);
-
custom_recyclerview.addHeaderView(custom_recyclerview.getHeaderView(), headerAndFooterWrapper);
-
custom_recyclerview.addFooterView(custom_recyclerview.getFooterView(), headerAndFooterWrapper);
-
custom_recyclerview.setOnRefreshListener(new OnRecyclerRefreshListener());
-
}
-
private class OnRecyclerRefreshListener implements RefreshRecyclerView.OnRefreshListener {
-
@Override
-
public void onPullDownRefresh() {
-
// 执行下拉刷新操作,一般是联网更新数据
-
new Thread(new Runnable() {
-
@Override
-
public void run() {
-
SystemClock.sleep(2000);
-
handler.sendEmptyMessage(REFRESH);
-
}
-
}).start();
-
}
-
@Override
-
public void onLoadingMore() {
-
// 执行上拉加载操作,一般是联网请求更多分页数据
-
new Thread(new Runnable() {
-
@Override
-
public void run() {
-
SystemClock.sleep(2000);
-
handler.sendEmptyMessage(LOADMORE);
-
}
-
}).start();
-
}
-
}
-
}
-
<pre name="code" class="java">
-
public class RefreshRecyclerViewAdapter extends RecyclerView.Adapter<RefreshRecyclerViewAdapter.ViewHolder> {
-
private Context mContext;
-
private List<String> datas;
-
public RefreshRecyclerViewAdapter(Context mContext) {
-
this.mContext = mContext;
-
datas = new ArrayList<>();
-
for(int i = 1; i <= 20; i++) {
-
datas.add("我是Content :"+i);
-
}
-
}
-
@Override
-
public RefreshRecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-
View convertView = View.inflate(mContext, R.layout.item_recycler, null);
-
return new ViewHolder(convertView);
-
}
-
@Override
-
public void onBindViewHolder(RefreshRecyclerViewAdapter.ViewHolder holder, final int position) {
-
String data = datas.get(position);
-
holder.tv_content.setText(data);
-
}
-
@Override
-
public int getItemCount() {
-
return datas.size();
-
}
-
public class ViewHolder extends RecyclerView.ViewHolder {
-
TextView tv_content;
-
public ViewHolder(final View itemView) {
-
super(itemView);
-
tv_content = (TextView) itemView.findViewById(R.id.tv);
-
}
-
}
-
}
这里还用到一个分割线的类,RecyclerView默认是没有分割线的,这段代码可以直接粘贴使用
-
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
-
public final static int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
-
public final static int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
-
private int mOrientation;
-
private Drawable mDivider;
-
private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
-
public DividerItemDecoration(Context context, int mOrientation) {
-
final TypedArray a = context.obtainStyledAttributes(ATTRS);
-
mDivider = a.getDrawable(0);
-
a.recycle();
-
setOrientation(mOrientation);
-
}
-
private void setOrientation(int mOrientation) {
-
if (mOrientation != VERTICAL_LIST && mOrientation != HORIZONTAL_LIST) {
-
throw new IllegalArgumentException("Invalid orientation");
-
} else {
-
this.mOrientation = mOrientation;
-
}
-
}
-
@Override
-
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
-
super.onDraw(c, parent, state);
-
if (mOrientation == VERTICAL_LIST) {
-
drawVertical(c, parent);
-
} else if (mOrientation == HORIZONTAL_LIST) {
-
drawHorizontal(c, parent);
-
}
-
}
-
private void drawVertical(Canvas c, RecyclerView parent) {
-
int left = parent.getPaddingLeft();
-
int right = parent.getWidth() - parent.getPaddingRight();
-
int childCount = parent.getChildCount();
-
for (int i = 0; i < childCount; i++) {
-
View child = parent.getChildAt(i);
-
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
-
int top = child.getBottom() + params.bottomMargin;
-
int bottom = top + mDivider.getIntrinsicHeight();
-
mDivider.setBounds(left, top, right, bottom);
-
mDivider.draw(c);
-
}
-
}
-
private void drawHorizontal(Canvas c, RecyclerView parent) {
-
int top = parent.getPaddingTop();
-
int bottom = parent.getHeight() - parent.getPaddingBottom();
-
int childCount = parent.getChildCount();
-
for (int i = 0; i < childCount; i++) {
-
View child = parent.getChildAt(i);
-
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
-
int left = child.getRight() + params.rightMargin;
-
int right = left + mDivider.getIntrinsicHeight();
-
mDivider.setBounds(left,top,right,bottom);
-
mDivider.draw(c);
-
}
-
}
-
@Override
-
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
-
// super.getItemOffsets(outRect, view, parent, state);
-
if(mOrientation == VERTICAL_LIST) {
-
outRect.set(0,0,0,mDivider.getIntrinsicHeight());
-
}else{
-
outRect.set(0,0,mDivider.getIntrinsicHeight(),0);
-
}
-
}
-
}