package com.anthony.pullrefreshview;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Handler;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.OverScroller;
/**
* Created by Anthony on 2016/7/18.
* 实现对子view 的上拉和下拉的监听实现,提供下拉和下拉的视图和接口
*
*/
public class PullToRefreshView extends ViewGroup{
private LayoutInflater mInflater;
private OverScroller mScroller;
private OnRefreshListener mListener;
/**
* 头部View
*/
private View header;
private BaseIndicator mHeaderIndicator;
private String mHeaderIndicatorClassName;
/**
* 尾部View
*/
private View footer;
private BaseIndicator mFooterIndicator;
private String mFooterIndicatorClassName;
/**
* 内容View
*/
private View contentView;
private int mHeaderActionPosition;
private int mFooterActionPosition;
private int mHeaderHoldingPosition;
private int mFooterHoldingPosition;
private boolean isPullDownEnable = true;
private boolean isPullUpEnable = true;
private float mLastX;
private float mLastY;
private float deltaX = 0;
private float deltaY = 0;
private int IDLE = 0;
private int PULL_DOWN = 1;
private int PULL_UP = 2;
private int AUTO_SCROLL_PULL_DOWN = 3; //自动刷新时的状态
private int mStatus = IDLE;
private boolean isLoading = false;
private long mStartLoadingTime;
public PullToRefreshView(Context context) {
super(context);
}
public PullToRefreshView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public PullToRefreshView(Context context, AttributeSet attrs) {
super(context, attrs);
mInflater = LayoutInflater.from(context);
mScroller = new OverScroller(context);
//获取自定义属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PullToRefresh);
if (ta.hasValue(R.styleable.PullToRefresh_header_indicator)) {
mHeaderIndicatorClassName = ta.getString(R.styleable.PullToRefresh_header_indicator);
}
if (ta.hasValue(R.styleable.PullToRefresh_footer_indicator)) {
mFooterIndicatorClassName = ta.getString(R.styleable.PullToRefresh_footer_indicator);
}
ta.recycle();
}
@Override
protected void onFinishInflate() {
if (getChildCount() != 1) {
throw new RuntimeException("The child of VIPullToRefresh should be only one!!!");
}
contentView = getChildAt(0);
setPadding(0, 0, 0, 0);
contentView.setPadding(0, 0, 0, 0);
mHeaderIndicator = getIndicator(mHeaderIndicatorClassName);
if (mHeaderIndicator == null) {
mHeaderIndicator = new DefaultHeader();
}
header = mHeaderIndicator.createView(mInflater, this);
mFooterIndicator = getIndicator(mFooterIndicatorClassName);
if (mFooterIndicator == null) {
mFooterIndicator = new DefaultFooter();
}
footer = mFooterIndicator.createView(mInflater, this);
contentView.bringToFront();
super.onFinishInflate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (getChildCount() > 0) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
mHeaderActionPosition = header.getMeasuredHeight() / 3 + header.getMeasuredHeight();
mFooterActionPosition = footer.getMeasuredHeight() / 3 + footer.getMeasuredHeight();
mHeaderHoldingPosition = header.getMeasuredHeight();
mFooterHoldingPosition = footer.getMeasuredHeight();
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (contentView != null) {
if (header != null) {
header.layout(0, -header.getMeasuredHeight(), getWidth(), 0);
}
if (footer != null) {
footer.layout(0, getHeight(), getWidth(), getHeight() + footer.getMeasuredHeight());
}
// if (header != null) {
// header.layout(0, 0, getWidth(), header.getMeasuredHeight());
// }
// if (footer != null) {
// footer.layout(0, getHeight() - footer.getMeasuredHeight(), getWidth(), getHeight());
// }
contentView.layout(0, 0, contentView.getMeasuredWidth(), contentView.getMeasuredHeight());
}
}
private boolean isInControl = false;
private boolean isNeedIntercept;
private boolean isNeedIntercept() {
if (deltaY > 0 && isContentScrollToTop() || getScrollY() < 0 - 10) {
mStatus = PULL_DOWN;
return true;
}
if (deltaY < 0 && isContentScrollToBottom() || getScrollY() > 0 + 10) {
mStatus = PULL_UP;
return true;
}
return false;
}
/**
* dispatchTouchEvent主要用于记录触摸事件的初始状态
* 因为这里是所有触摸事件的入口函数所有事件都会经过这里
* <p>
* 因此如果你需要观测整个事件序列从开始到最后,无论它是否被本ViewGroup拦截,那么请在这个函数中进行
* <p>
* 为什么不在onInterceptTouchEvent中观测??
* 因为在onInterceptTouchEvent中进行记录可能漏掉一些事件
* 例如:当布局内部控件在onTouchEvent函数中对DOWN返回true,那么后续的MOVE事件和UP事件就可能不会传入onInterceptTouchEvent函数中
* 再比如:当本ViewGroup自身对第一个MOVE事件进行拦截即onInterceptTouchEvent返回true时后续的MOVE和UP事件都不会传入onInterceptTouchEvent函数中
* 而是直接进入onTouchEvent中去
*/
private VelocityTracker mVelocityTracker;
private float yVelocity;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
dealMultiTouch(ev);
final int action = MotionEventCompat.getActionMasked(ev);
switch (action) {
case MotionEvent.ACTION_DOWN:
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
} else {
mVelocityTracker.clear();
}
mVelocityTracker.addMovement(ev);
break;
case MotionEvent.ACTION_MOVE:
mVelocityTracker.addMovement(ev);
mVelocityTracker.computeCurrentVelocity(500);
yVelocity = mVelocityTracker.getYVelocity();
isNeedIntercept = isNeedIntercept();
if (isNeedIntercept && !isInControl) {
//把内部控件的事件转发给本控件处理
isInControl = true;
ev.setAction(MotionEvent.ACTION_CANCEL);
MotionEvent ev2 = MotionEvent.obtain(ev);
dispatchTouchEvent(ev);
ev2.setAction(MotionEvent.ACTION_DOWN);
return dispatchTouchEvent(ev2);
}
break;
case MotionEvent.ACTION_UP:
/**
* 为什么将本ViewGroup自动返回初始位置的触发函数放在dispatchTouchEvent中?
* 由于下面onTouchEvent代码中ACTION_MOVE时有一段对本ViewGroup当前事件控制权转移给内部控件的代码
* 因此这会使得最后的Up event不会到本ViewGroup中的onTouchEvent中去
* 所以只能将autoBackToOriginalPosition()前移到dispatchTouchEvent()中来
*/
autoBackToPosition();
isNeedIntercept = false;
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return isNeedIntercept;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
switch (action) {
case MotionEvent.ACTION_DOWN:
return true;
case MotionEvent.ACTION_MOVE:
if (!isPullDownEnable && deltaY > 0 && getScrollY() <= 0) {
break;
}
if (!isPullUpEnable && deltaY < 0 && getScrollY() >= 0) {
break;
}
if (isNeedIntercept) {
if (mStatus == PULL_DOWN && getScrollY() > 0) {
break;
}
if (mStatus == PULL_UP && getScrollY() < 0) {
break;
}
scrollBy(0, (int) (-getMoveFloat(yVelocity, deltaY)));
updateIndicator();
} else {
ev.setAction(MotionEvent.ACTION_DOWN);
dispatchTouchEvent(ev);
isInControl = false;
}
break;
case MotionEvent.ACTION_UP:
autoBackToPosition();
return true;
}
return true;
}
/**
* 处理多点触控的情况
* 记录手指按下和移动时的各种相关数值
* mActivePointerId为有效手指的ID,后续所有移动数值均来自这个手指
* 当前active的手指只有一个且为后按下的那个
*/
private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
public void dealMultiTouch(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float y = MotionEventCompat.getY(ev, pointerIndex);
mLastX = x;
mLastY = y;
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
break;
}
case MotionEvent.ACTION_MOVE: {
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float y = MotionEventCompat.getY(ev, pointerIndex);
//下拉时deltaY为正,上拉时deltaY为负
deltaX = x - mLastX;
deltaY = y - mLastY;
mLastY = y;
mLastX = x;
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mActivePointerId = MotionEvent.INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_POINTER_DOWN: {
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
if (pointerId != mActivePointerId) {
mLastX = MotionEventCompat.getX(ev, pointerIndex);
mLastY = MotionEventCompat.getY(ev, pointerIndex);
mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
}
break;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastX = MotionEventCompat.getX(ev, newPointerIndex);
mLastY = MotionEventCompat.getY(ev, newPointerIndex);
mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
}
break;
}
}
}
@Override
public void computeScroll() {
// 先判断mScroller滚动是否完成
if (mScroller.computeScrollOffset()) {
// 这里调用View的scrollTo()完成实际的滚动
scrollTo(0, mScroller.getCurrY());
// 立即重绘View实现滚动效果
invalidate();
}
}
/**
* 速度控制函数
* 当手指移动速度越接近10000px每500毫秒时获得的控件移动距离越小
* 1.5f为权重,越大速度控制越明显,表现为用户越不容易拉动本控件,即拉动越吃力
* <p>
* 若不进行速度控制,将可能导致一系列问题,其中包括
* 用户下拉一段距离,突然很快加速上拉控件,这时footer将被拖出,但此时内部控件并未到达它的底部
* 这样显示不符合上拉加载的逻辑
*/
private float getMoveFloat(float velocity, float org) {
return ((10000f - Math.abs(velocity)) / 10000f * org) / 1.5f;
}
/**
* 判断该回到初始状态还是Loading状态
*/
private void autoBackToPosition() {
if (mStatus == PULL_DOWN && Math.abs(getScrollY()) < mHeaderActionPosition) {
autoBackToOriginalPosition();
} else if (mStatus == PULL_DOWN && Math.abs(getScrollY()) > mHeaderActionPosition) {
autoBackToLoadingPosition();
} else if (mStatus == PULL_UP && Math.abs(getScrollY()) < mFooterActionPosition) {
autoBackToOriginalPosition();
} else if (mStatus == PULL_UP && Math.abs(getScrollY()) > mFooterActionPosition) {
autoBackToLoadingPosition();
}
}
/**
* 回到Loading状态
*/
private void autoBackToLoadingPosition() {
mStartLoadingTime = System.currentTimeMillis();
if (mStatus == PULL_DOWN) {
mScroller.startScroll(0, getScrollY(), 0, -getScrollY() - mHeaderHoldingPosition, 400);
invalidate();
if (!isLoading) {
isLoading = true;
mListener.onRefresh();
}
}
if (mStatus == PULL_UP) {
mScroller.startScroll(0, getScrollY(), 0, mFooterHoldingPosition - getScrollY(), 400);
invalidate();
if (!isLoading) {
isLoading = true;
mListener.onLoadMore();
}
}
loadingIndicator();
}
/**
* 回到初始状态
*/
private void autoBackToOriginalPosition() {
mScroller.startScroll(0, getScrollY(), 0, -getScrollY(), 400);
invalidate();
this.postDelayed(new Runnable() {
@Override
public void run() {
restoreIndicator();
mStatus = IDLE;
}
}, 500);
}
private boolean isContentScrollToTop() {
return !ViewCompat.canScrollVertically(contentView, -1);
}
private boolean isContentScrollToBottom() {
return !ViewCompat.canScrollVertically(contentView, 1);
}
/**
* 在拖动过程中调用Indicator(Header或Footer)的接口函数完成相应的指示性变化
* 例如:下拉刷新、放开刷新的变化
*/
private void updateIndicator() {
if (mStatus == PULL_DOWN && deltaY > 0) {
if (Math.abs(getScrollY()) > mHeaderActionPosition) {
mHeaderIndicator.onAction();
}
} else if (mStatus == PULL_DOWN && deltaY < 0) {
if (Math.abs(getScrollY()) < mHeaderActionPosition) {
mHeaderIndicator.onUnaction();
}
} else if (mStatus == PULL_UP && deltaY < 0) {
if (Math.abs(getScrollY()) > mFooterActionPosition) {
mFooterIndicator.onAction();
}
} else if (mStatus == PULL_UP && deltaY > 0) {
if (Math.abs(getScrollY()) < mFooterActionPosition) {
mFooterIndicator.onUnaction();
}
}
}
/**
* 本控件自动返回初始位置后恢复Indicator到初始状态
*/
private void restoreIndicator() {
mHeaderIndicator.onRestore();
mFooterIndicator.onRestore();
}
/**
* 本控件自动返回Loading位置后设置Indicator为Loading状态
*/
private void loadingIndicator() {
if (mStatus == PULL_DOWN) {
mHeaderIndicator.onLoading();
}
if (mStatus == PULL_UP) {
mFooterIndicator.onLoading();
}
}
public void onFinishLoading() {
long delta = System.currentTimeMillis() - mStartLoadingTime;
if (delta > 2000) {
isLoading = false;
autoBackToOriginalPosition();
} else {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
isLoading = false;
autoBackToOriginalPosition();
}
}, 1000);
}
}
public void onAutoRefresh() {
mStartLoadingTime = System.currentTimeMillis();
mStatus = PULL_DOWN;
mScroller.startScroll(0, getScrollY(), 0, -mHeaderHoldingPosition, 400);
invalidate();
if (!isLoading) {
isLoading = true;
mListener.onRefresh();
}
loadingIndicator();
}
/**
* Interface
*/
public interface OnRefreshListener {
void onRefresh();
void onLoadMore();
}
private BaseIndicator getIndicator(String className) {
if (!TextUtils.isEmpty(className)) {
try {
Class clazz = Class.forName(className);
return (BaseIndicator) clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* Getter and Setter
*/
public void setListener(OnRefreshListener mListener) {
this.mListener = mListener;
}
public boolean isPullDownEnable() {
return isPullDownEnable;
}
public void setPullDownEnable(boolean pullDownEnable) {
isPullDownEnable = pullDownEnable;
}
public boolean isPullUpEnable() {
return isPullUpEnable;
}
public void setPullUpEnable(boolean pullUpEnable) {
isPullUpEnable = pullUpEnable;
}
}