Bootstrap

android 自定义刷新控件,android自定义下拉刷新和上拉加载控件

项目链接:点击打开链接

前段时间和同事一起写了一个下拉刷新和上拉加载的控件。该控件实现的功能的功能还是挺多的。支持自定义头部和底布局,同时还处理了NestScrolling机制的嵌套滑动的一些东西。我们先来看下效果图吧。

1.主页面布局展示

c35560cbebd2ca3dab604b5d02792b4d.png

2.支持RecycleView的刷新和加载效果。

5eafca32f50915b5e989bdefc4927b03.gif

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 ~

项目链接:点击打开链接

;