转载请注明出处 http://blog.csdn.net/u011453163/article/details/62896816
本篇内容在我的另一篇博客
RecyclerView学习(1) 添加头部和尾部
的基础上改进的,文末会附上源码。
RecyclerView 的上拉加载和下拉刷新 已经是一个老话题了,网上也有很多实现方式,本着学习的态度就当做一个新的课题来研究以及一路上的坑。
RecyclerView的上拉加载下拉刷新 实现的方式目前有这么几种
1 使用官方组件 SwipeRefreshLayout 包裹RecyclerView 实现下拉接口 ,上拉加载则是监听滚动到底部来处理一些UI变化
2 加载和刷新 都是借用RecyclerView 原始头部和尾部来实现。
3 自定义一个布局包裹RecyclerView 来实现。
本篇内容使用的是第二种方式。效果图
实现刷新和加载的核心思想
1.添加可刷新头部和尾部,如何添加头部和尾部参照我的上一篇博客RecyclerView学习(1) 添加头部和尾部
2 上拉和下拉效果,通过改变头部和尾部的高度实现,弹性效果使用的是属性动画还实现的
关于如何添加头部和尾部就不过多解释了,参照第一篇,只是做了一些小调整使得头部和尾部始终在两端。
本篇主要分析上拉下拉的实现。
下拉刷新
1 判断滑动到顶部
2 解决手势和RecyclerView自身可滚动的冲突
3 回弹效果
判断滑动到顶部的方法我知道的有这么几种
1 通过此方法
canScrollVertically(int direction)
但是在头部或尾部隐藏的情况下无效
2 通过监听滚动计算
3 通过获取position==0 的Item 的top==0来判断
refreshView.getTop()==0
本篇用的是第三种
onTouchEvent(MotionEvent e)
@Override
public boolean onTouchEvent(MotionEvent e) {
if (rdownY == -1) {
rdownY = e.getRawY();
}
if(ldownY==-1){
ldownY=e.getRawY();
}
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float rdy = e.getRawY() - rdownY;
float ldy =ldownY- e.getRawY();
if (refreshView == null) {
refreshView = getLayoutManager().findViewByPosition(0);
if (refreshView instanceof BaseRefreshHeader) {
((BaseRefreshHeader) refreshView).setOnRefreshListener(onRefreshListener);
}
}
if(loadView==null){
loadView = getLayoutManager().findViewByPosition(getLayoutManager().getItemCount()-1);
if(loadView instanceof BaseLoadFooter){
((BaseLoadFooter) loadView).setOnLoadListener(onLoadListener);
}
}
if (allowPulldown()&&!isLoading()&&supportPullRefresh) {
((BaseRefreshHeader) refreshView).pulldown((int) (rdy / 2));
if (refreshView.getHeight() >= 0 && rdy > 0) {
return false;
}
}else{
rdownY = e.getRawY();
}
if (allowPullup()&&!isRefreshing()&&supportPullLoad){
((BaseLoadFooter) loadView).pullup((int) (ldy/ 2));
offsetChildrenVertical(1);//消除上拉时抖动 抖动的原因猜测是 view的高度是向下延伸的
if(loadView.getHeight()>0&&ldy>0){
scrollToPosition(getLayoutManager().getItemCount()-1);
return false;
}
}else {
ldownY=e.getRawY();
}
break;
case MotionEvent.ACTION_UP:
if (refreshView instanceof BaseRefreshHeader&& refreshView.getParent() != null&&supportPullRefresh) {
((BaseRefreshHeader) refreshView).loosen();
}else if(loadView instanceof BaseLoadFooter && loadView.getParent() != null&&supportPullLoad){
((BaseLoadFooter) loadView).loosen();
}
rdownY = -1;
ldownY=-1;
break;
}
return super.onTouchEvent(e);
}
onTouchEvent事件逻辑包括了上拉和下拉。
1 初始化 按下坐标 默认-1 只要小于0都可以
if (rdownY == -1) {
rdownY = e.getRawY();
}
if(ldownY==-1){
ldownY=e.getRawY();
}
2 MotionEvent.ACTION_MOVE 计算滑动差量以及获取第一个和最后一个Item
float rdy = e.getRawY() - rdownY;
float ldy =ldownY- e.getRawY();
if (refreshView == null) {
refreshView = getLayoutManager().findViewByPosition(0);
if (refreshView instanceof BaseRefreshHeader) {
((BaseRefreshHeader) refreshView).setOnRefreshListener(onRefreshListener);
}
}
if(loadView==null){
loadView = getLayoutManager().findViewByPosition(getLayoutManager().getItemCount()-1);
if(loadView instanceof BaseLoadFooter){
((BaseLoadFooter) loadView).setOnLoadListener(onLoadListener);
}
}
3 可下拉上拉判断 (可支持下拉和上拉的逻辑是 到达顶部,当前状态不是正在刷新或者加载 以及是否支持上拉加载下拉刷新)
/**是否正在刷新*/
private boolean isRefreshing(){
if(refreshView instanceof BaseRefreshHeader){
return ((BaseRefreshHeader) refreshView).getStatus()==BaseRefreshHeader.STATUS_REFRESHING;
}
return false;
}
/**是否正在加载*/
private boolean isLoading(){
if(loadView instanceof BaseLoadFooter){
return ((BaseLoadFooter) loadView).getStatus()==BaseLoadFooter.STATUS_LOADING;
}
return false;
}
/**允许下拉操作*/
private boolean allowPulldown(){
return refreshView instanceof BaseRefreshHeader
&& refreshView.getParent() != null
&& refreshView.getTop()==0;
}
/**允许上拉操作*/
private boolean allowPullup(){
return loadView instanceof BaseLoadFooter
&& loadView.getParent()!=null
&& loadView.getBottom()==getLayoutManager().getHeight();
}
4 解决改变高度的同时和自身滑动冲突(在下拉或上拉之后往反方向滑动时 在减少高度的同时RecyclerView也在滚动 这样高度计算就会出问题 而且体验极差)处理方式 在下拉或上拉之后 屏蔽RecyclerView自身事件处理
if (allowPulldown()&&!isLoading()&&supportPullRefresh) {
((BaseRefreshHeader) refreshView).pulldown((int) (rdy / 2));
if (refreshView.getHeight() >= 0 && rdy > 0) {
return false;
}
}else{
rdownY = e.getRawY();
}
if (allowPullup()&&!isRefreshing()&&supportPullLoad){
((BaseLoadFooter) loadView).pullup((int) (ldy/ 2));
offsetChildrenVertical(1);//消除上拉时抖动 抖动的原因猜测是 view的高度是向下延伸的
if(loadView.getHeight()>0&&ldy>0){
scrollToPosition(getLayoutManager().getItemCount()-1);
return false;
}
}else {
ldownY=e.getRawY();
}
5 上拉和下拉之后 当高度大于0以后 只改变高度屏蔽滚动
if (refreshView.getHeight() >= 0 && rdy > 0) {
return false;
}
if(loadView.getHeight()>0&&ldy>0){
scrollToPosition(getLayoutManager().getItemCount()-1); return false;
}
这里下拉和上拉有点点区别 因为View的高度是向下延伸的 所以在上拉的时候要调用以下方法来实现拉伸效果 ,scrollToPosition是将使item完整的出现在界面上 如果已在界面上则无效
scrollToPosition(getLayoutManager().getItemCount()-1);
(另外刚开始上拉时会出现微微抖动 猜测是和View高度向下延伸的原因 此方法可消除抖动 )
offsetChildrenVertical(1);
5 处理了下拉的操作之后就是手指抬起之后刷新的逻辑了,这里使用抽象类基础头部和尾部,方便头部和尾部的扩展 BaseRefreshHeader,BaseLoadFooter。实现逻辑基本是一样的。
public abstract class BaseRefreshHeader extends RelativeLayout {
public static final int STATUS_NORMAL = -1;//无刷新状态
public static final int STATUS_REFRESHING = 1;//刷新中
private int status = STATUS_NORMAL;
private OnRefreshListener onRefreshListener;
public BaseRefreshHeader(Context context) {
this(context, null);
}
public BaseRefreshHeader(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BaseRefreshHeader(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,0));
}
public void setHeaderHeight(int height) {
if (height > 0) {
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) getLayoutParams();
layoutParams.height = height;
setLayoutParams(layoutParams);
} else {
RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) getLayoutParams();
layoutParams.height = 0;
setLayoutParams(layoutParams);
status = STATUS_NORMAL;
reset();
}
}
public void pulldown(int height) {
if(height<0){
return;
}
if(status==STATUS_REFRESHING){
setHeaderHeight(refreshHeight()+height);
move(refreshHeight()+height);
}else {
setHeaderHeight(height);
move(height);
}
if(status!=STATUS_REFRESHING) {
if (height > refreshHeight()) {
refresh(true);
} else {
refresh(false);
}
}
}
public int getStatus() {
return status;
}
public void loosen() {
if (getHeight()>=refreshHeight()) {
animMove(100, getHeight(), refreshHeight());
if(status!=STATUS_REFRESHING){
status = STATUS_REFRESHING;
if (onRefreshListener!=null){
onRefreshListener.onRefresh();
loosenAndRefresh();
}
}
} else {
animMove(50, getHeight(), 0);
}
}
/**拉伸过程*/
protected abstract void move(int height);
/**可刷新的高度*/
protected abstract int refreshHeight();
/**是否已经达到可刷新状态*/
protected abstract void refresh(boolean canRefresh);
/**松开并刷新*/
protected abstract void loosenAndRefresh();
/**复位*/
protected abstract void reset();
private void animMove(long duratuon, int... values) {
ValueAnimator valueAnimator = ValueAnimator.ofInt(values);
valueAnimator.setDuration(duratuon);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int dy = (int) animation.getAnimatedValue();
setHeaderHeight(dy);
}
});
valueAnimator.start();
}
public void complete(){
status = STATUS_NORMAL;
animMove(200, getHeight(), 0);
}
public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
this.onRefreshListener = onRefreshListener;
}
}
头部抽象类 几个抽象方法主要用于扩展 有几个关键点
1 初始高度 0 隐藏头部
public BaseRefreshHeader(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,0));
}
2 弹性效果使用的是属性动画实现的
private void animMove(long duratuon, int... values) {
ValueAnimator valueAnimator = ValueAnimator.ofInt(values);
valueAnimator.setDuration(duratuon);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int dy = (int) animation.getAnimatedValue();
setHeaderHeight(dy);
}
});
valueAnimator.start();
}
3 一些满足大部分需求的抽象方法
/**拉伸过程*/
protected abstract void move(int height);
/**可刷新的高度*/
protected abstract int refreshHeight();
/**是否已经达到可刷新状态*/
protected abstract void refresh(boolean canRefresh);
/**松开并刷新*/
protected abstract void loosenAndRefresh();
/**复位*/
protected abstract void reset();
4 刷新过程中下拉不会重复刷新
public void pulldown(int height) {
if(height<0){
return;
}
if(status==STATUS_REFRESHING){
setHeaderHeight(refreshHeight()+height);
move(refreshHeight()+height);
}else {
setHeaderHeight(height);
move(height);
}
if(status!=STATUS_REFRESHING) {
if (height > refreshHeight()) {
refresh(true);
} else {
refresh(false);
}
}
}
头部的逻辑很简单 主要是通过高度高确定刷新的时机。
到这里核心的逻辑就分析完了,接下来看看如何来使用,使用也是及其简单的
rvConetents= (ExpandRecyclerView) findViewById(R.id.recyclerview);
layoutManager=new LinearLayoutManager(this);
rvConetents.setLayoutManager(layoutManager);
rvConetents.setSupportPullRefresh(true);
rvConetents.setSupportPullLoad(true);
for (int i=0;i<8;i++){
ss.add("字符初始"+i);
}
testAdapter=new TestAdapter(this,ss);
rvConetents.setAdapter(testAdapter);
rvConetents.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh() {
Log.d("RecyclerViewActivity", "刷新");
rvConetents.postDelayed(new Runnable() {
@Override
public void run() {
ss.clear();
for (int i=0;i<2;i++){
ss.add("字符新加"+i);
}
testAdapter.notifyDataSetChanged();
rvConetents.refreshComplete();
}
},3000);
}
});
rvConetents.setOnLoadListener(new OnLoadListener() {
@Override
public void onLoad() {
Log.d("RecyclerViewActivity", "加载");
rvConetents.postDelayed(new Runnable() {
@Override
public void run() {
rvConetents.loadComplete();
for (int i=0;i<10;i++){
ss.add("字符新加"+i);
}
testAdapter.notifyDataSetChanged();
}
},3000);
}
});
使用和普通的RecyclerView基本没什么区别 只是多了下拉刷新 上拉加载的一些监听回调。这里就不细讲了
**
**
java.lang.IllegalArgumentException: called detach on an already detached child ViewHolder{22a52cd8 position=7 id=-1, oldPos=-1, pLpos:-1 scrap [attachedScrap] tmpDetached no parent}
at android.support.v7.widget.RecyclerView$5.detachViewFromParent(RecyclerView.java:737)
at android.support.v7.widget.ChildHelper.detachViewFromParent(ChildHelper.java:284)
at android.support.v7.widget.RecyclerView$LayoutManager.detachViewInternal(RecyclerView.java:7593)
at android.support.v7.widget.RecyclerView$LayoutManager.detachViewAt(RecyclerView.java:7586)
猜测在刷新的时候RecyclerView复用Item的时候 出现了复用到不存在的item 出现奔溃
if (vh != null) {
if (vh.isTmpDetached() && !vh.shouldIgnore()) {
throw new IllegalArgumentException("called detach on an already"
+ " detached child " + vh);
}
if (DEBUG) {
Log.d(TAG, "tmpDetach " + vh);
}
vh.addFlags(ViewHolder.FLAG_TMP_DETACHED);
}
出现问题时 google了好久没有找到好的处理方式 猜测既然是复用出了问题干脆就把childview都清除好了
private final RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
if(getChildCount()>1) {
removeViews(1, getChildCount()-1);
}
expandAdapter.notifyDataSetChanged();
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
expandAdapter.notifyItemRangeInserted(positionStart, itemCount);
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
expandAdapter.notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
expandAdapter.notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
expandAdapter.notifyItemRangeRemoved(positionStart, itemCount);
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
expandAdapter.notifyItemMoved(fromPosition, toPosition);
}
};
在数据改变的时候清除所有的childview。
**
**
不支持扩展的都是耍流氓,高仿一把京东的下拉刷新效果。看效果(录屏软件有点问题 掉帧了)
直接看代码 主要利用了基类的几个抽象方法
public class JDRefreshHeader extends BaseRefreshHeader {
ImageView ivPeople,ivGoods,ivGoodsAnim;
TextView tv;
public JDRefreshHeader(Context context) {
this(context,null);
}
public JDRefreshHeader(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public JDRefreshHeader(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutInflater.from(context).inflate(R.layout.jd_refresh_header,this);
ivPeople= (ImageView) findViewById(R.id.iv_people);
ivGoods= (ImageView) findViewById(R.id.iv_goods);
ivGoodsAnim= (ImageView) findViewById(R.id.iv_goods_anim);
tv= (TextView) findViewById(R.id.tv);
ivGoodsAnim.setVisibility(GONE);
}
@Override
protected void move(int height) {
}
@Override
protected int refreshHeight() {
return 140;
}
@Override
protected void refresh(boolean canRefresh) {
if(canRefresh){
tv.setText("松开刷新");
}else {
tv.setText("下拉刷新");
}
}
@Override
protected void loosenAndRefresh() {
tv.setText("更新中...");
ivGoods.setVisibility(GONE);
ivPeople.setVisibility(GONE);
ivGoodsAnim.setVisibility(VISIBLE);
ivGoodsAnim.setBackgroundResource(R.drawable.jd_anim);
AnimationDrawable animationDrawable;
animationDrawable= (AnimationDrawable) ivGoodsAnim.getBackground();
animationDrawable.start();
}
@Override
protected void reset() {
tv.setText("");
ivGoods.setVisibility(VISIBLE);
ivPeople.setVisibility(VISIBLE);
ivGoodsAnim.setVisibility(GONE);
}
}
继承基类 实现几个抽象方法 提供一个刷新高度(很关键) 刷新头部 设置非常简单。
rvConetents.setAdapter(testAdapter);
rvConetents.addHeadView(new JDRefreshHeader(this));
到此RecyclerView的上拉加载下拉刷新就解析完成,有什么不对的地方欢迎指正。