项目链接:点击打开链接
前段时间和同事一起写了一个下拉刷新和上拉加载的控件。该控件实现的功能的功能还是挺多的。支持自定义头部和底布局,同时还处理了NestScrolling机制的嵌套滑动的一些东西。我们先来看下效果图吧。
1.主页面布局展示
2.支持RecycleView的刷新和加载效果。
3.支持listView的刷新和加载效果。
4.支持ScrollView的刷新和加载效果
5.支持NestScrolling机制的刷新和加载效果。
6.内容固定模式
1.控件的使用方式
从代码中可以看到,使用pullLayout的时候,我们一般要设置三个view,第一个view是内容布局,这里我们可以设置recycycleview、listview、scrollview甚至LinearLayout等其他布局。第二个view和第三个view分别代表头布局和底布局,这是我们自定义的布局,头布局和底部局的具体事件操作需要我们自行在里面定义。
当然也不是必须设置三个view,但是至少设置一个view来代表内容布局,否则这个空间将没有什么意义了,啥都没有还能用来干嘛呢?第二和第三个view是我们可以自行设置的。
2.pullLayout的代码
import android.content.Context;
import android.graphics.Point;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;
import java.util.ArrayList;
/**
* @description 可能具有顶部刷新和底部加载功能的布局
* @note 视图的添加顺序为内容、头部(非必要)、底部(非必要)
**/
public class PullLayout extends ViewGroup implements NestedScrollingParent,NestedScrollingChild {
//内容视图
private View mContentView;
//顶部刷新的时候会显示的视图
private View mHeaderView;
//底部加载的时候会显示的视图
private View mFooterView;
//当前是否在触摸状态下
private boolean isOnTouch;
private PullLayoutOption mOption;
//头部视图的高度
private int mHeaderHeight;
//底部视图的高度
private int mFooterHeight;
//上次的触摸事件坐标
private Point mLastPoint;
//当前偏移量
private int mCurrentOffset;
//上次的偏移量
private int mPrevOffset;
private int mTouchSlop;
//刷新和加载更多的回调
private ArrayList mRefreshListeners;
private ArrayList mLoadMoreListeners;
//当前是否在刷新中
private boolean isRefreshing;
//当前是否在加载中
private boolean isLoading;
//缓慢滑动工作者
private ScrollerWorker mScroller;
//主要用于标记当前事件的意义
private boolean canUpIntercept;
private boolean canDownIntercept;
//一次拦截事件的时候当前是否可以顶部或底部刷新
private boolean canUp;
private boolean canDown;
//当前是否处于嵌套滑动中
private boolean isNestedScrolling;
private NestedScrollingParentHelper mParentHelper;
private NestedScrollingChildHelper mChildHelper;
public PullLayout(Context context) {
this(context, null);
}
public PullLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PullLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initData();
}
private void initData() {
mOption = new PullLayoutOption();
mLastPoint = new Point();
ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
mRefreshListeners = new ArrayList<>();
mLoadMoreListeners = new ArrayList<>();
mScroller = new ScrollerWorker(getContext());
mParentHelper = new NestedScrollingParentHelper(this);
mChildHelper = new NestedScrollingChildHelper(this);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
int childCount = getChildCount();
switch (childCount) {
case 1://这种时候默认只有一个内容视图
mContentView = getChildAt(0);
break;
case 2://默认优先支持顶部刷新
mContentView = getChildAt(0);
mHeaderView = getChildAt(1);
break;
case 3:
mContentView = getChildAt(0);
mHeaderView = getChildAt(1);
mFooterView = getChildAt(2);
break;
default:
throw new IllegalArgumentException("必须包括1到3个子视图");
}
checkHeaderAndFooterAndAddListener();
}
/**
* 检查头部和底部是否为监听,是的话添加到监听回调列表中
*/
private void checkHeaderAndFooterAndAddListener() {
if (mHeaderView instanceof IRefreshListener) {
mRefreshListeners.add((IRefreshListener) mHeaderView);
}
if (mFooterView instanceof ILoadMoreListener) {
mLoadMoreListeners.add((ILoadMoreListener) mFooterView);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildWithMargins(mContentView, widthMeasureSpec, 0, heightMeasureSpec, 0);
MarginLayoutParams lp = null;
if (null != mHeaderView) {
measureChildWithMargins(mHeaderView, widthMeasureSpec, 0, heightMeasureSpec, 0);
lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
mHeaderHeight = mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
if (null != mFooterView) {
measureChildWithMargins(mFooterView, widthMeasureSpec, 0, heightMeasureSpec, 0);
lp = (MarginLayoutParams) mFooterView.getLayoutParams();
mFooterHeight = mFooterView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left, top;
MarginLayoutParams lp;
lp = (MarginLayoutParams) mContentView.getLayoutParams();
left = (l + getPaddingLeft() + lp.leftMargin);
if (mOption.isContentFixed()) {
top = (t + getPaddingTop() + lp.topMargin);
}else{
top = (t + getPaddingTop() + lp.topMargin) + mCurrentOffset;
}
//画内容布局
mContentView.layout(left, top, left + mContentView.getMeasuredWidth(), top + mContentView.getMeasuredHeight());
//画headerView布局
if (null != mHeaderView) {
lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
left = (l + getPaddingLeft() + lp.leftMargin);
top = (t + getPaddingTop() + lp.topMargin) - mHeaderHeight + mCurrentOffset;
mHeaderView.layout(left, top, left + mHeaderView.getMeasuredWidth(), top + mHeaderView.getMeasuredHeight());
}
//画footerView布局
if (null != mFooterView) {
lp = (MarginLayoutParams) mFooterView.getLayoutParams();
left = (l + getPaddingLeft() + lp.leftMargin);
top = (b - getPaddingBottom() + lp.topMargin) + mCurrentOffset;
mFooterView.layout(left, top, left + mFooterView.getMeasuredWidth(), top + mFooterView.getMeasuredHeight());
}
}
/**
* 事件分发的处理,判断是否拦截滑动事件,当满足下拉刷新和上拉加载的时候,会返回true代表父布局拦截滑动事件并调用onTouchvent消耗
* 这个时候会显示出头布局或者底布局
**/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (!isEnabled() || !hasHeaderOrFooter() || isRefreshing || isLoading || isNestedScrolling) {
return false;
}
switch (MotionEventCompat.getActionMasked(event)) {
case MotionEvent.ACTION_MOVE:
int x = (int) event.getX();
int y = (int) event.getY();
int deltaY = (y - mLastPoint.y);
int dy = Math.abs(deltaY);
int dx = Math.abs(x - mLastPoint.x);
Log.d(getClass().getSimpleName(), "dx-->" + dx + "--dy-->" + dy + "--touchSlop-->" + mTouchSlop);
if (dy > mTouchSlop && dy >= dx) {
canUp = mOption.canUpToDown();//通过option文件里面定义能从上往下拉,即下拉刷新。外部调用
canDown = mOption.canDownToUp();//通过option文件里面定义能从下往上拉,即上拉加载。外部调用
Log.d(getClass().getSimpleName(), "canUp-->" + canUp + "--canDown-->" + canDown + "--deltaY-->" + deltaY);
canUpIntercept = (deltaY > 0 && canUp);
canDownIntercept = (deltaY < 0 && canDown);
return canUpIntercept || canDownIntercept;//能上拉刷新或者下拉加载的时候拦截时间,父布局消耗,否则底布局消耗。
}
return false;
}
mLastPoint.set((int) event.getX(), (int) event.getY());
return false;
}
/**
*处理下拉刷新和上拉加载
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled() || !hasHeaderOrFooter() || isRefreshing || isLoading || isNestedScrolling) {
return false;
}
switch (MotionEventCompat.getActionMasked(event)) {
case MotionEvent.ACTION_MOVE:
isOnTouch = true;
updatePos((int) (event.getY() - mLastPoint.y));
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
isOnTouch = false;
if (mCurrentOffset > 0) {
tryPerformRefresh();
} else {
tryPerformLoading();
}
break;
}
mLastPoint.set((int) event.getX(), (int) event.getY());
return true;
}
/**
* 修改偏移量,改变视图位置
*
* @param deltaY 当前位置的偏移量
*/
private void updatePos(int deltaY) {
if (!hasHeaderOrFooter() || deltaY == 0) {//不需要偏移
return;
}
if (isOnTouch) {
if (!canUp && (mCurrentOffset + deltaY > 0)) {//此时偏移量不应该>0
deltaY = (0 - mCurrentOffset);
} else if (!canDown && (mCurrentOffset + deltaY < 0)) {//此时偏移量不应该<0
deltaY = (0 - mCurrentOffset);
}
}
mPrevOffset = mCurrentOffset;
mCurrentOffset += deltaY;
mCurrentOffset = Math.max(Math.min(mCurrentOffset, mOption.getMaxDownOffset()), mOption.getMaxUpOffset());
deltaY = mCurrentOffset - mPrevOffset;
if (deltaY == 0) {//不需要偏移
return;
}
callUIPositionChangedListener(mPrevOffset, mCurrentOffset);
if (!mOption.isContentFixed()) {
mContentView.offsetTopAndBottom(deltaY);
}
if (null != mHeaderView) {
mHeaderView.offsetTopAndBottom(deltaY);
}
if (null != mFooterView) {
mFooterView.offsetTopAndBottom(deltaY);
}
invalidate();
}
/**
* 是否有头部或者底部视图
*
* @return true是
*/
private boolean hasHeaderOrFooter() {
return null != mHeaderView || null != mFooterView;
}
/**
* 尝试处理加载更多
*/
private void tryPerformLoading() {
if (isOnTouch || isLoading || isNestedScrolling) {
return;
}
if (mCurrentOffset <= mOption.getLoadMoreOffset()) {
startLoading();
} else {
mScroller.trySmoothScrollToOffset(0);
}
}
/**
* 尝试处理刷新回调
*/
private void tryPerformRefresh() {
if (isOnTouch || isRefreshing || isNestedScrolling) {//触摸中或者刷新中不进行回调
return;
}
if (mCurrentOffset >= mOption.getRefreshOffset()) {
startRefreshing();
} else {//没有达到刷新条件,还原状态
mScroller.trySmoothScrollToOffset(0);
}
}
/**
* 处理刷新
*/
private void startRefreshing() {
isRefreshing = true;
callRefreshBeginListener();
mScroller.trySmoothScrollToOffset(mOption.getRefreshOffset());
}
/**
* 处理加载
*/
private void startLoading() {
isLoading = true;
callLoadMoreBeginListener();
mScroller.trySmoothScrollToOffset(mOption.getLoadMoreOffset());
}
/**
* 回调刷新和加载的各种监听
**/
private void callRefreshBeginListener() {
for (IRefreshListener listener : mRefreshListeners) {
listener.onRefreshBegin();
}
}
private void callRefreshCompleteListener() {
for (IRefreshListener listener : mRefreshListeners) {
listener.onRefreshComplete();
}
}
private void callUIPositionChangedListener(int oldOffset, int newOffset) {
for (IRefreshListener listener : mRefreshListeners) {
listener.onUIPositionChanged(oldOffset, newOffset);
}
for (ILoadMoreListener loadMoreListener : mLoadMoreListeners) {
loadMoreListener.onUIPositionChanged(oldOffset, newOffset);
}
}
private void callLoadMoreBeginListener() {
for (ILoadMoreListener listener : mLoadMoreListeners) {
listener.onLoadMoreBegin();
}
}
private void callLoadMoreCompleteListener() {
for (ILoadMoreListener listener : mLoadMoreListeners) {
listener.onLoadMoreComplete();
}
}
/** end **/
/**
* 添加和移除监听
**/
public void addRefreshListener(IRefreshListener listener) {
mRefreshListeners.add(listener);
}
public void removeRefreshListener(IRefreshListener listener) {
mRefreshListeners.remove(listener);
}
public void addLoadMoreListener(ILoadMoreListener listener) {
mLoadMoreListeners.add(listener);
}
public void removeLoadMoreListener(ILoadMoreListener listener) {
mLoadMoreListeners.remove(listener);
}
/** end **/
/**
* 配置相关
**/
public void setOnCheckHandler(PullLayoutOption.OnCheckHandler handler) {
mOption.setOnCheckHandler(handler);
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
//只接收竖直方向上面的嵌套滑动
boolean isVerticalScroll = (nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL);
boolean canTouchMove = isEnabled() && hasHeaderOrFooter();
return isVerticalScroll && canTouchMove;
}
@Override
public void onStopNestedScroll(View child) {
mParentHelper.onStopNestedScroll(child);
if (isNestedScrolling) {
isNestedScrolling = false;
isOnTouch = false;
if (mCurrentOffset >= mOption.getRefreshOffset()) {
startRefreshing();
} else if(mCurrentOffset <= mOption.getLoadMoreOffset()){
startLoading();
} else {//没有达到刷新条件,还原状态
mScroller.trySmoothScrollToOffset(0);
}
}
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
mParentHelper.onNestedScrollAccepted(child, target, axes);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
if (isNestedScrolling) {
canUp = mOption.canUpToDown();
canDown = mOption.canDownToUp();
int minOffset = canDown?mOption.getMaxUpOffset():0;
int maxOffset = canUp?mOption.getMaxDownOffset():0;
int nextOffset = (mCurrentOffset - dy);
int sureOffset = Math.min(Math.max(minOffset,nextOffset),maxOffset);
int deltaY = sureOffset - mCurrentOffset;
consumed[1] = (-deltaY);
updatePos(deltaY);
}
dispatchNestedPreScroll(dx, dy, consumed, null);
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
boolean canTouch = !isLoading && !isRefreshing && !isOnTouch;
if (dyUnconsumed != 0 && canTouch) {
canUp = mOption.canUpToDown();
canDown = mOption.canDownToUp();
boolean canUpToDown = (canUp && dyUnconsumed < 0);
boolean canDownToUp = (canDown && dyUnconsumed > 0);
if(canUpToDown || canDownToUp){
isOnTouch = true;
isNestedScrolling = true;
updatePos(-dyUnconsumed);
dyConsumed = dyUnconsumed;
dyUnconsumed = 0;
}
}
dispatchNestedScroll(dxConsumed,dxUnconsumed,dyConsumed,dyUnconsumed,null);
}
/**
* 处理SmoothScroll
*/
private class ScrollerWorker implements Runnable {
public static final int DEFAULT_SMOOTH_TIME = 400;//ms
public static final int AUTO_REFRESH_SMOOTH_TIME = 200;//ms,自动刷新和自动加载时布局弹出时间
private int mSmoothScrollTime;
private int mLastY;//上次的Y坐标偏移量
private Scroller mScroller;//间隔计算执行者
private Context mContext;//上下文
private boolean isRunning;//当前是否运行中
public ScrollerWorker(Context mContext) {
this.mContext = mContext;
mScroller = new Scroller(mContext);
mSmoothScrollTime = DEFAULT_SMOOTH_TIME;
}
public void setSmoothScrollTime(int mSmoothScrollTime) {
this.mSmoothScrollTime = mSmoothScrollTime;
}
@Override
public void run() {
boolean isFinished = (!mScroller.computeScrollOffset() || mScroller.isFinished());
if (isFinished) {
end();
} else {
int y = mScroller.getCurrY();
int deltaY = (y - mLastY);
boolean isDown = ((mPrevOffset == mOption.getRefreshOffset()) && deltaY > 0);
boolean isUp = ((mPrevOffset == mOption.getLoadMoreOffset()) && deltaY < 0);
if (isDown || isUp) {//不需要进行多余的滑动
end();
return;
}
updatePos(deltaY);
mLastY = y;
post(this);
}
}
/**
* 尝试缓慢滑动到指定偏移量
*
* @param targetOffset 需要滑动到的偏移量
*/
public void trySmoothScrollToOffset(int targetOffset) {
if (!hasHeaderOrFooter()) {
return;
}
endScroller();
removeCallbacks(this);
mLastY = 0;
int deltaY = (targetOffset - mCurrentOffset);
mScroller.startScroll(0, 0, 0, deltaY, mSmoothScrollTime);
isRunning = true;
post(this);
}
/**
* 结束Scroller
*/
private void endScroller() {
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
}
mScroller.abortAnimation();
}
/**
* 停止并且还原滑动工作
*/
public void end() {
removeCallbacks(this);
endScroller();
isRunning = false;
mLastY = 0;
}
}
}
代码量比较多,800多行,因为很多的相关配置希望用户自己去外面自由灵活的调用。所以里面写了比较多的配置性代码,都是比较容易读懂的,大家不必担心。上面贴出来的是部分代码,完整的源码大家自己去看项目里面。贴出来主要是实现逻辑类的代码。
3.实现原理
1.获取控件里面的3个view
protected void onFinishInflate() {
super.onFinishInflate();
int childCount = getChildCount();
switch (childCount) {
case 1://这种时候默认只有一个内容视图
mContentView = getChildAt(0);
break;
case 2://默认优先支持顶部刷新
mContentView = getChildAt(0);
mHeaderView = getChildAt(1);
break;
case 3:
mContentView = getChildAt(0);
mHeaderView = getChildAt(1);
mFooterView = getChildAt(2);
break;
default:
throw new IllegalArgumentException("必须包括1到3个子视图");
}
详细地我们在上面见过,主要用来在onlayout方法中调用放置。
2.我们看看在onLayout中是怎么样放置这三个view的呢?
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left, top;
MarginLayoutParams lp;
lp = (MarginLayoutParams) mContentView.getLayoutParams();
left = (l + getPaddingLeft() + lp.leftMargin);
if (mOption.isContentFixed()) {
top = (t + getPaddingTop() + lp.topMargin);
}else{
top = (t + getPaddingTop() + lp.topMargin) + mCurrentOffset;
}
//画内容布局
mContentView.layout(left, top, left + mContentView.getMeasuredWidth(), top + mContentView.getMeasuredHeight());
//画headerView布局
if (null != mHeaderView) {
lp = (MarginLayoutParams) mHeaderView.getLayoutParams();
left = (l + getPaddingLeft() + lp.leftMargin);
top = (t + getPaddingTop() + lp.topMargin) - mHeaderHeight + mCurrentOffset;
mHeaderView.layout(left, top, left + mHeaderView.getMeasuredWidth(), top + mHeaderView.getMeasuredHeight());
}
//画footerView布局
if (null != mFooterView) {
lp = (MarginLayoutParams) mFooterView.getLayoutParams();
left = (l + getPaddingLeft() + lp.leftMargin);
top = (b - getPaddingBottom() + lp.topMargin) + mCurrentOffset;
mFooterView.layout(left, top, left + mFooterView.getMeasuredWidth(), top + mFooterView.getMeasuredHeight());
}
}
总得来说最重要的是mCurrentOffset这个变量,我们画头部局headerview和底布局footerview都是通过这个mCurrentOffset这个变量放置的。对于下拉刷新来说,就是当出发下拉刷新开始的时候起,头布局下拉的时候的偏移量。相反对于上拉加载的时候,就是底布局上拉的时候偏移量。说到这里大家估计就会有幡然大悟的感觉,好像还挺好实现的。确实,等会我们就可以通过事件拦截机制和事件处理,看pullLaout到底是否出发了下拉刷新和上拉加载的条件。看看事件拦截的代码
/**
* 事件分发的处理,判断是否拦截滑动事件,当满足下拉刷新和上拉加载的时候,会返回true代表父布局拦截滑动事件并调用onTouchvent消耗
* 这个时候会显示出头布局或者底布局
**/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (!isEnabled() || !hasHeaderOrFooter() || isRefreshing || isLoading || isNestedScrolling) {
return false;
}
switch (MotionEventCompat.getActionMasked(event)) {
case MotionEvent.ACTION_MOVE:
int x = (int) event.getX();
int y = (int) event.getY();
int deltaY = (y - mLastPoint.y);
int dy = Math.abs(deltaY);
int dx = Math.abs(x - mLastPoint.x);
Log.d(getClass().getSimpleName(), "dx-->" + dx + "--dy-->" + dy + "--touchSlop-->" + mTouchSlop);
if (dy > mTouchSlop && dy >= dx) {
canUp = mOption.canUpToDown();//通过option文件里面定义能从上往下拉,即下拉刷新。外部调用
canDown = mOption.canDownToUp();//通过option文件里面定义能从下往上拉,即上拉加载。外部调用
Log.d(getClass().getSimpleName(), "canUp-->" + canUp + "--canDown-->" + canDown + "--deltaY-->" + deltaY);
canUpIntercept = (deltaY > 0 && canUp);
canDownIntercept = (deltaY < 0 && canDown);
return canUpIntercept || canDownIntercept;//能上拉刷新或者下拉加载的时候拦截时间,父布局消耗,否则底布局消耗。
}
return false;
}
mLastPoint.set((int) event.getX(), (int) event.getY());
return false;
}
/**
*处理下拉刷新和上拉加载
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled() || !hasHeaderOrFooter() || isRefreshing || isLoading || isNestedScrolling) {
return false;
}
switch (MotionEventCompat.getActionMasked(event)) {
case MotionEvent.ACTION_MOVE:
isOnTouch = true;
updatePos((int) (event.getY() - mLastPoint.y));
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
isOnTouch = false;
if (mCurrentOffset > 0) {//偏移量大于0,即下拉,代表下拉刷新
tryPerformRefresh();
} else {
tryPerformLoading();//偏移量小于0,即上拉,代表上拉加载
}
break;
}
mLastPoint.set((int) event.getX(), (int) event.getY());
return true;
}
当满足下拉刷新和上拉加载的时候,会返回true代表父布局拦截滑动事件并调用onTouchvent消耗,这个时候会显示出头布局或者底布局,通过option文件里面定义下拉刷新或者上拉加载。外部调用,让使用者在外部自行设置下拉刷新和上拉加载的条件。我们来看看option文件的代码
package fanjh.mine.pulllayout;
/**
* @author fanjh
* @date 2017/4/27 9:44
* @description 具有刷新和加载功能的布局的一些基础配置
* @note
* 1.需要在代码中设置
* 2.整体的偏移量以初始位置为准,向下为真,向上为负
**/
public class PullLayoutOption {
//触发顶部刷新的偏移量
private int mRefreshOffset;
//布局向下滑动的最大偏移量,要大于等于mRefreshOffset才能使刷新有效
private int mMaxDownOffset;
//滑动的系数,主要是在手指滑动的距离的基础上乘以系数,从而产生阻尼或放大的感觉
private float mMoveRatio;
//触发底部加载更多的偏移量,这个是负数
private int mLoadMoreOffset;
//布局向上滑动的最大偏移量,这个是负数,要小于等于mLoadMoreOffset才能使加载有效
private int mMaxUpOffset;
//内容视图位置是否固定,即不会随着手指滑动而移动位置,类似于SwipeRefreshLayout
private boolean isContentFixed;
//校验监听
private OnCheckHandler mOnCheckHandler;
public interface OnCheckHandler{
boolean canUpTpDown();
boolean canDownToUp();
}
public PullLayoutOption() {
}
public void setOnCheckHandler(OnCheckHandler mOnCheckHandler) {
this.mOnCheckHandler = mOnCheckHandler;
}
public PullLayoutOption setRefreshOffset(int mRefreshOffset) {
this.mRefreshOffset = mRefreshOffset;
return this;
}
public PullLayoutOption setMaxDownOffset(int mMaxDownOffset) {
this.mMaxDownOffset = mMaxDownOffset;
return this;
}
public PullLayoutOption setMoveRatio(float mMoveRatio) {
this.mMoveRatio = mMoveRatio;
return this;
}
public PullLayoutOption setLoadMoreOffset(int mLoadMoreOffset) {
this.mLoadMoreOffset = (-mLoadMoreOffset);//要转为负数
return this;
}
public PullLayoutOption setMaxUpOffset(int mMaxUpOffset) {
this.mMaxUpOffset = (-mMaxUpOffset);//要转为负数
return this;
}
public PullLayoutOption setContentFixed(boolean contentFixed) {
isContentFixed = contentFixed;
return this;
}
public int getRefreshOffset() {
return mRefreshOffset;
}
public int getMaxDownOffset() {
return mMaxDownOffset;
}
public float getMoveRatio() {
return mMoveRatio;
}
public int getLoadMoreOffset() {
return mLoadMoreOffset;
}
public int getMaxUpOffset() {
return mMaxUpOffset;
}
public boolean isContentFixed() {
return isContentFixed;
}
/**
* 是否可以因为顶部刷新从而使得手指可以从上到下滑动
* @return true可以,否则不行
*/
public boolean canUpToDown(){
//没有手动设置监听
return null == mOnCheckHandler || mOnCheckHandler.canUpTpDown();
}
/**
* 是否可以因为底部加载从而使得手指可以从下到上滑动
* @return true可以,否则不行
*/
public boolean canDownToUp(){
//没有手动设置监听
return null == mOnCheckHandler || mOnCheckHandler.canDownToUp();
}
}
外部是实现OnCheckHandler里面的canUpTpDown()和canDownToUp()方法来定义下拉刷新和上拉加载的条件的。当满足这两条件的时候,我们可以看到canUpIntercept 或者canDownIntercept为true的时候,这个时候就是会下拉刷新出头部或者底布局,具体是怎么样的呢,我们看看OnTouchEvent里面的updatePos(int position)方法
/**
* 修改偏移量,改变视图位置
*
* @param deltaY 当前位置的偏移量
*/
private void updatePos(int deltaY) {
if (!hasHeaderOrFooter() || deltaY == 0) {//不需要偏移
return;
}
if (isOnTouch) {
if (!canUp && (mCurrentOffset + deltaY > 0)) {//此时偏移量不应该>0
deltaY = (0 - mCurrentOffset);
} else if (!canDown && (mCurrentOffset + deltaY < 0)) {//此时偏移量不应该<0
deltaY = (0 - mCurrentOffset);
}
}
mPrevOffset = mCurrentOffset;
mCurrentOffset += deltaY;
mCurrentOffset = Math.max(Math.min(mCurrentOffset, mOption.getMaxDownOffset()), mOption.getMaxUpOffset());
deltaY = mCurrentOffset - mPrevOffset;
if (deltaY == 0) {//不需要偏移
return;
}
callUIPositionChangedListener(mPrevOffset, mCurrentOffset);
if (!mOption.isContentFixed()) {
mContentView.offsetTopAndBottom(deltaY);
}
if (null != mHeaderView) {
mHeaderView.offsetTopAndBottom(deltaY);
}
if (null != mFooterView) {
mFooterView.offsetTopAndBottom(deltaY);
}
invalidate();
}
可以看出来,我们是在里面改变了mCurrentOffset的值,然后调用invalidate方法重新绘制,pullLayout就会重新布局里面的三个view了。当然里面是有设置下拉刷新和上拉加载的最大偏移量的。出发这个值得时候,就不会继续偏移了。然后我们手抬起来的时候,触发MotionEvent.ACTION_UP,里面回调用tryPerformRefresh或者tryPerformLoading方法,这两个方法处理刷新和加载的方法。里面会让headerView或者footerView先平缓移动到相应位置,里面的平缓滑动我们需要自己处理一下,用的是我们自己写的一个类。
/**
* 处理SmoothScroll
*/
private class ScrollerWorker implements Runnable {
public static final int DEFAULT_SMOOTH_TIME = 400;//ms
public static final int AUTO_REFRESH_SMOOTH_TIME = 200;//ms,自动刷新和自动加载时布局弹出时间
private int mSmoothScrollTime;
private int mLastY;//上次的Y坐标偏移量
private Scroller mScroller;//间隔计算执行者
private Context mContext;//上下文
private boolean isRunning;//当前是否运行中
public ScrollerWorker(Context mContext) {
this.mContext = mContext;
mScroller = new Scroller(mContext);
mSmoothScrollTime = DEFAULT_SMOOTH_TIME;
}
public void setSmoothScrollTime(int mSmoothScrollTime) {
this.mSmoothScrollTime = mSmoothScrollTime;
}
@Override
public void run() {
boolean isFinished = (!mScroller.computeScrollOffset() || mScroller.isFinished());
if (isFinished) {
end();
} else {
int y = mScroller.getCurrY();
int deltaY = (y - mLastY);
boolean isDown = ((mPrevOffset == mOption.getRefreshOffset()) && deltaY > 0);
boolean isUp = ((mPrevOffset == mOption.getLoadMoreOffset()) && deltaY < 0);
if (isDown || isUp) {//不需要进行多余的滑动
end();
return;
}
updatePos(deltaY);
mLastY = y;
post(this);
}
}
/**
* 尝试缓慢滑动到指定偏移量
*
* @param targetOffset 需要滑动到的偏移量
*/
public void trySmoothScrollToOffset(int targetOffset) {
if (!hasHeaderOrFooter()) {
return;
}
endScroller();
removeCallbacks(this);
mLastY = 0;
int deltaY = (targetOffset - mCurrentOffset);
mScroller.startScroll(0, 0, 0, deltaY, mSmoothScrollTime);
isRunning = true;
post(this);
}
/**
* 结束Scroller
*/
private void endScroller() {
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
}
mScroller.abortAnimation();
}
/**
* 停止并且还原滑动工作
*/
public void end() {
removeCallbacks(this);
endScroller();
isRunning = false;
mLastY = 0;
}
}
里面也是调用scroller.startScroll方法进行缓慢滑动的,我们可以自行设置滑动时间,里面也是通过updatePos改变位置的。最后调用刷新方法,至于刷新时干什么,里面也有回调,当然后这个必须加回调啦,否则用户用你这个干嘛呢,哈哈。。。
最后的最后,还有一个嵌套滑动机制。大家不懂得就先去了解一下嵌套滑动机制到底是怎么样的,本文就不讲解了。pullLayout里面也有一个自定义属性disableScrolling来让我们选择是否支持嵌套滑动。大家可以去下载源码看下,里面都有详细地注释。
ok,就写到这里啦,欢迎大家找出里面的bug ~
项目链接:点击打开链接