1.概述
Android中的下拉刷新是我们很常见的的操作,一般列表页的下拉刷新都是SwipeRefreshLayout嵌套RecycleView完成,其中的原理就是运用了NestedScrolling嵌套滚动机制,具体可参考鸿洋老师的这篇文章https://blog.csdn.net/lmj623565791/article/details/52204039.
public class SwipeRefreshLayout extends ViewGroup implements NestedScrollingParent,
NestedScrollingChild {
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
从上可以看到SwipeRefreshLayout 实现了NestedScrollingParent,RecyclerView 实现了NestedScrollingChild2 ,我们用的最多的下拉刷新就是使用了这个机制.
2.效果展示
3.具体实现
- 布局
刷新的头部局
- ReFreshParent
public class ReFreshParent extends ViewGroup implements NestedScrollingParent {
public ReFreshParent(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
View view = LayoutInflater.from(context).inflate(R.layout.rv_header, this, false);
mProgress = view.findViewById(R.id.pb);
mTvTip = view.findViewById(R.id.tv_tip);
mArrow = view.findViewById(R.id.iv_arrow);
addView(view);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mHeader.layout(0, -mHeaderHeight, getWidth(), 0);
mContent.layout(0, 0, getWidth(), getBottom());
}
在构造中加载出头部局,然后添加该布局,onLayout方法中将头部局摆放到列表上
- 滑动操作
嵌套滑动过程中滑动事件是由子孩子传给父布局的,这里面就是由RecycleView传过来的滑动事件,在父布局中我们只重写了这二个实现方法:
/**
* 父View是否允许嵌套滑动
*
* @param child 包含嵌套滑动父类的子View
* @param target 实现嵌套滑动的子View
* @param nestedScrollAxes 嵌套滑动方向,水平竖直或都支持
*/
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
//当前状态不是正在刷新中当前布局就可以滑动
return mStatus != STATUS.REFRESHING;
}
/**
* 嵌套滑动子View滑动之前的准备工作
*
* @param target 实现嵌套滑动的子View
* @param dx 水平方向上嵌套滑动的子View滑动的总距离
* @param dy 竖直方向上嵌套滑动的子View滑动的总距离
* @param consumed consumed[0]水平方向与consumed[1]竖直方向上父View消耗(滑动)的距离
*/
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
//上滑头部局逐渐隐藏
boolean hiddenRefresh = dy > 0 && getScrollY() <= 0;
//下滑头部局逐渐出现
boolean showRefresh = dy < 0 && getScrollY() >= -mHeaderHeight && !ViewCompat.canScrollVertically(target, -1);
if (getScrollY() >= -mHeaderHeight / 2) {
mStatus = STATUS.PULLREFRESH;
} else {
mStatus = STATUS.RELEASEREFRESH;
}
refreshByStatus(mStatus);
if (hiddenRefresh || showRefresh) {
scrollBy(0, dy);
consumed[1] = dy;
}
}
- onStartNestedScroll方法中,如果当前状态不是正在刷新中就允许父布局可以可以接收滑动事件.
- onNestedPreScroll方法中,上滑头部局逐渐隐藏或下滑头部局逐渐出现这二种状态下就调用scrollBy方法滑动父布局来隐藏或显示头部局.
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
if (getScrollY() <= -mHeaderHeight / 2) {
mStatus = STATUS.REFRESHING;
refreshByStatus(mStatus);
} else {
scrollTo(0, 0);
}
}
return false;
}
- 在手抬起的时候根据头部局的位置来判断是否刷新
- 根据状态来动态改变头部局的展示
4. 父布局完整代码:
public class ReFreshParent extends LinearLayout implements NestedScrollingParent {
private boolean isMeasured;
//刷新的头部局
private View mHeader;
//RecycleView列表页
private RecyclerView mContent;
//头部局的高度
private int mHeaderHeight;
private Scroller mScroller;
//进度条
private ProgressBar mProgress;
//刷新提示
private TextView mTvTip;
//指示箭头
private ImageView mArrow;
//上次刷新的时间
private long mLastTime;
private TextView mTvTime;
private ValueAnimator mUpAnim;
private ValueAnimator mDownAnim;
private enum STATUS {
//刷新中,下拉刷新,释放刷新
REFRESHING, PULLREFRESH, RELEASEREFRESH
}
//当前刷新状态
private STATUS mStatus = STATUS.PULLREFRESH;
private STATUS mLastStatus;
public ReFreshParent(Context context) {
this(context, null);
}
public ReFreshParent(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
View view = LayoutInflater.from(context).inflate(R.layout.rv_header, this, false);
mProgress = view.findViewById(R.id.pb);
mTvTip = view.findViewById(R.id.tv_tip);
mArrow = view.findViewById(R.id.iv_arrow);
mTvTime = view.findViewById(R.id.tv_time);
addView(view);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mHeader.layout(0, -mHeaderHeight, getWidth(), 0);
mContent.layout(0, 0, getWidth(), getBottom());
LogUtils.LogE("getBottom =" + getBottom());
}
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes) {
//当前状态不是正在刷新中当前布局就可以滑动
return mStatus != STATUS.REFRESHING;
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
//上滑头部局逐渐隐藏
boolean hiddenRefresh = dy > 0 && getScrollY() <= 0;
//下滑头部局逐渐出现
boolean showRefresh = dy < 0 && getScrollY() >= -mHeaderHeight && !ViewCompat.canScrollVertically(target, -1);
if (getScrollY() >= -mHeaderHeight / 2) {
mStatus = STATUS.PULLREFRESH;
} else {
mStatus = STATUS.RELEASEREFRESH;
}
refreshByStatus(mStatus);
if (hiddenRefresh || showRefresh) {
scrollBy(0, dy);
consumed[1] = dy;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!isMeasured) {
mHeader = getChildAt(0);
mContent = (RecyclerView) getChildAt(1);
measureChildren(widthMeasureSpec, heightMeasureSpec);
//获取头部局的高度
mHeaderHeight = mHeader.getMeasuredHeight();
isMeasured = true;
//recycleView手抬起时的监听
mContent.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
if (getScrollY() <= -mHeaderHeight / 2) {
mStatus = STATUS.REFRESHING;
refreshByStatus(mStatus);
} else {
scrollTo(0, 0);
}
}
return false;
}
});
mDownAnim = ValueAnimator.ofFloat(180, 360);
mDownAnim.setDuration(500);
mDownAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mArrow.setRotation((Float) animation.getAnimatedValue());
}
});
mUpAnim = ValueAnimator.ofFloat(0, 180);
mUpAnim.setDuration(500);
mUpAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mArrow.setRotation((Float) animation.getAnimatedValue());
}
});
}
}
/**
* 根据状态去改变
*/
private void refreshByStatus(STATUS status) {
if (mLastStatus != status) {
switch (status) {
case PULLREFRESH:
if (mLastStatus != null) {
mTvTip.setText("下拉刷新");
mDownAnim.start();
}
break;
case REFRESHING:
mTvTip.setText("正在刷新");
mProgress.setVisibility(VISIBLE);
mArrow.setVisibility(INVISIBLE);
mLastTime = System.currentTimeMillis();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
mProgress.setVisibility(INVISIBLE);
mArrow.setVisibility(VISIBLE);
scrollTo(0, 0);
mStatus = STATUS.PULLREFRESH;
mLastStatus = null;
mTvTime.setText("上次刷新时间:" + formartTime(mLastTime));
//刷新完毕
if (mRefreshCompleteListener != null)
mRefreshCompleteListener.refreshed();
}
}, 1500);
break;
case RELEASEREFRESH:
mTvTip.setText("释放刷新");
mUpAnim.start();
break;
}
mLastStatus = status;
}
}
private String formartTime(long time) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(time));
}
@Override
public void scrollTo(int x, int y) {
if (y <= -mHeaderHeight) {
y = -mHeaderHeight;
} else if (y >= 0) {
y = 0;
}
super.scrollTo(x, y);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
invalidate();
}
}
public interface RefreshCompleteListener {
void refreshed();
}
private RefreshCompleteListener mRefreshCompleteListener;
public void setRefreshCompleteListener(RefreshCompleteListener refreshCompleteListener) {
mRefreshCompleteListener = refreshCompleteListener;
}
}
项目地址:https://github.com/digtal/recycleview-study