package cn.mutils.app.ui; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.util.AttributeSet; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.view.animation.RotateAnimation; import android.webkit.WebView; import android.widget.AdapterView; import android.widget.RelativeLayout; import android.widget.ScrollView; import android.widget.TextView; import java.util.Timer; import java.util.TimerTask; import cn.mutils.app.R; /** * PullLayout refer to * http://blog.csdn.net/zhongkejingwang/article/details/38868463 */ @SuppressLint({"HandlerLeak", "InflateParams"}) @SuppressWarnings({"deprecation", "FieldCanBeLocal", "unused"}) public class PullLayout extends RelativeLayout { public interface OnRefreshListener { void onRefresh(PullLayout pullLayout); void onLoadMore(PullLayout pullLayout); } public static final int STATAE_INIT = 0; public static final int STATE_RELEASE_TO_REFRESH = 1; public static final int STATE_REFRESHING = 2; public static final int STATE_RELEASE_TO_LOAD = 3; public static final int STATE_LOADING = 4; public static final int STATE_DONE = 5; private int state = STATAE_INIT; private OnRefreshListener mListener; private float downY, lastY; /** * Pull down distance.pullDownY and pullUpY can not be same as zero */ public float pullDownY = 0; /** * Pull up distance */ private float pullUpY = 0; /** * Refresh distance */ private float refreshDist = 200; /** * Load more distance */ private float loadMoreDist = 200; private MyTimer timer; public float MOVE_SPEED = 8; /** * First layout */ private boolean isLayout = false; /** * Touch on refreshing */ private boolean isTouch = false; /** * 手指滑动距离与下拉头的滑动距离比,中间会随正切函数变化 */ private float radio = 2; /** * Arrow animation */ private RotateAnimation reverseAnimation; /** * Pull down head view */ private View refreshHeadView; /** * Pull down arrow view */ private View pullDownView; /** * Refreshing icon */ private View refreshingView; /** * Refresh state TextView */ private TextView refreshStateTextView; /** * Pull up foot view */ private View loadmoreFootView; /** * Pull up arrow view */ private View pullUpView; /** * Loading icon */ private View loadingView; /** * Load state TextView */ private TextView loadStateTextView; /** * Pull View who is real view for content */ private View pullView; /** * Filter one more touch point */ private int mEvents; /** * Whether it can pull down while touching */ private boolean canPullDown = true; /** * Whether it can pull up while touching */ private boolean canPullUp = true; /** * Whether pull down enabled */ protected boolean mPullDownEnabled = true; /** * Whether pull up enabled */ protected boolean mPullUpEnabled = true; /** * Policy for pull down and pull up */ protected PullPolicy mPolicy = new PullPolicy(); /** * Hander for back-rolling */ Handler updateHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { // 回弹速度随下拉距离moveDeltaY增大而增大 MOVE_SPEED = (float) (8 + 5 * Math.tan(Math.PI / 2 / getMeasuredHeight() * (pullDownY + Math.abs(pullUpY)))); if (!isTouch) { // 正在刷新,且没有往上推的话则悬停,显示"正在刷新..." if (state == STATE_REFRESHING && pullDownY <= refreshDist) { pullDownY = refreshDist + MOVE_SPEED; timer.cancel(); } else if (state == STATE_LOADING && -pullUpY <= loadMoreDist) { pullUpY = -loadMoreDist - MOVE_SPEED; timer.cancel(); } } if (pullDownY > 0) pullDownY -= MOVE_SPEED; else if (pullUpY < 0) pullUpY += MOVE_SPEED; if (pullDownY < 0) { // 已完成回弹 pullDownY = 0; pullDownView.clearAnimation(); // 隐藏下拉头时有可能还在刷新,只有当前状态不是正在刷新时才改变状态 if (state != STATE_REFRESHING && state != STATE_LOADING) changeState(STATAE_INIT); timer.cancel(); requestLayout(); } if (pullUpY > 0) { // 已完成回弹 pullUpY = 0; pullUpView.clearAnimation(); // 隐藏下拉头时有可能还在刷新,只有当前状态不是正在刷新时才改变状态 if (state != STATE_REFRESHING && state != STATE_LOADING) changeState(STATAE_INIT); timer.cancel(); } // 刷新布局,会自动调用onLayout requestLayout(); } }; public PullLayout(Context context) { super(context); init(context, null); } public PullLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public PullLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs); } private void init(Context context, AttributeSet attrs) { timer = new MyTimer(updateHandler); reverseAnimation = (RotateAnimation) AnimationUtils.loadAnimation(context, R.anim.pull_reverse); LayoutInflater inflater = LayoutInflater.from(context); refreshHeadView = inflater.inflate(R.layout.pull_refresh_head, null); loadmoreFootView = inflater.inflate(R.layout.pull_load_more, null); // 初始化下拉布局 pullDownView = refreshHeadView.findViewById(R.id.pull_icon); refreshStateTextView = (TextView) refreshHeadView.findViewById(R.id.state_tv); refreshingView = refreshHeadView.findViewById(R.id.refreshing_icon); // 初始化上拉布局 pullUpView = loadmoreFootView.findViewById(R.id.pullup_icon); loadStateTextView = (TextView) loadmoreFootView.findViewById(R.id.loadstate_tv); loadingView = loadmoreFootView.findViewById(R.id.loading_icon); if (attrs != null) { TypedArray typedArray = context.obtainStyledAttributes(R.styleable.PullLayout); int textSize = typedArray.getDimensionPixelSize(R.styleable.PullLayout_android_textSize, 0); if (textSize != 0) { refreshStateTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); loadStateTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); } ColorStateList textColor = typedArray.getColorStateList(R.styleable.PullLayout_android_textColor); if (textColor != null) { refreshStateTextView.setTextColor(textColor); loadStateTextView.setTextColor(textColor); } typedArray.recycle(); } refreshHeadView.setBackgroundDrawable(this.getBackground()); loadmoreFootView.setBackgroundDrawable(this.getBackground()); } public void setTextColor(int textColor) { refreshStateTextView.setTextColor(textColor); loadStateTextView.setTextColor(textColor); } public void setTextSize(float size) { refreshStateTextView.setTextSize(size); loadStateTextView.setTextSize(size); } public PullPolicy getPolicy() { return mPolicy; } public void setPolicy(PullPolicy policy) { mPolicy = policy; } public boolean isPullDownEnabled() { return mPullDownEnabled; } public boolean isPullUpEnabled() { return mPullUpEnabled; } public void setPullDownEnabled(boolean enabled) { if (mPullDownEnabled == enabled) { return; } mPullDownEnabled = enabled; changeState(STATE_DONE); hide(); } public void setPullUpEnabled(boolean enabled) { if (mPullUpEnabled == enabled) { return; } mPullUpEnabled = enabled; changeState(STATE_DONE); hide(); } public View getPullView() { return pullView; } public void ensureHeadFoot() { if (this.getChildCount() == 3) { if (this.getChildAt(0) != refreshHeadView) { throw new UnsupportedOperationException(); } if (this.getChildAt(1) != pullView) { throw new UnsupportedOperationException(); } if (this.getChildAt(2) != loadmoreFootView) { throw new UnsupportedOperationException(); } return; } if (this.getChildCount() != 1) { throw new UnsupportedOperationException("PullLayout must has only one child view"); } View v = this.getChildAt(0); if (v.getLayoutParams().height != LayoutParams.MATCH_PARENT) { throw new UnsupportedOperationException(); } pullView = v; this.addView(refreshHeadView, 0, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); this.addView(loadmoreFootView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } public void setOnRefreshListener(OnRefreshListener listener) { mListener = listener; } protected void onFinishInflate() { super.onFinishInflate(); ensureHeadFoot(); } private void hide() { timer.schedule(5); } public void onRefreshComplete() { changeState(STATE_DONE); hide(); } public void onLoadMoreComplete() { changeState(STATE_DONE); hide(); } private void changeState(int to) { state = to; switch (state) { case STATAE_INIT: // 下拉布局初始状态 refreshingView.setVisibility(View.GONE); refreshStateTextView.setText(R.string.pull_to_refresh); pullDownView.clearAnimation(); pullDownView.setVisibility(View.VISIBLE); // 上拉布局初始状态 loadingView.setVisibility(View.GONE); loadStateTextView.setText(R.string.pull_up_to_load); pullUpView.clearAnimation(); pullUpView.setVisibility(View.VISIBLE); break; case STATE_RELEASE_TO_REFRESH: // 释放刷新状态 refreshStateTextView.setText(R.string.pull_release_to_refresh); pullDownView.startAnimation(reverseAnimation); break; case STATE_REFRESHING: // 正在刷新状态 pullDownView.clearAnimation(); refreshingView.setVisibility(View.VISIBLE); pullDownView.setVisibility(View.GONE); refreshStateTextView.setText(R.string.pull_refreshing); break; case STATE_RELEASE_TO_LOAD: // 释放加载状态 loadStateTextView.setText(R.string.pull_release_to_load); pullUpView.startAnimation(reverseAnimation); break; case STATE_LOADING: // 正在加载状态 pullUpView.clearAnimation(); loadingView.setVisibility(View.VISIBLE); pullUpView.setVisibility(View.GONE); loadStateTextView.setText(R.string.pull_loading); break; case STATE_DONE: // 刷新或加载完毕,啥都不做 break; } } /** * 不限制上拉或下拉 */ private void releasePull() { canPullDown = true; canPullUp = true; } @Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: downY = ev.getY(); lastY = downY; timer.cancel(); mEvents = 0; releasePull(); break; case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_POINTER_UP: // 过滤多点触碰 mEvents = -1; break; case MotionEvent.ACTION_MOVE: if (mEvents == 0) { if (mPullDownEnabled && (pullDownY > 0 || (mPolicy.canPullDown(pullView) && canPullDown && state != STATE_LOADING))) { // 可以下拉,正在加载时不能下拉 // 对实际滑动距离做缩小,造成用力拉的感觉 pullDownY = pullDownY + (ev.getY() - lastY) / radio; if (pullDownY < 0) { pullDownY = 0; canPullDown = false; canPullUp = true; } if (pullDownY > getMeasuredHeight()) pullDownY = getMeasuredHeight(); if (state == STATE_REFRESHING) { // 正在刷新的时候触摸移动 isTouch = true; } } else if (mPullUpEnabled && (pullUpY < 0 || (mPolicy.canPullUp(pullView) && canPullUp && state != STATE_REFRESHING))) { // 可以上拉,正在刷新时不能上拉 pullUpY = pullUpY + (ev.getY() - lastY) / radio; if (pullUpY > 0) { pullUpY = 0; canPullDown = true; canPullUp = false; } if (pullUpY < -getMeasuredHeight()) pullUpY = -getMeasuredHeight(); if (state == STATE_LOADING) { // 正在加载的时候触摸移动 isTouch = true; } } else releasePull(); } else mEvents = 0; lastY = ev.getY(); // 根据下拉距离改变比例 radio = (float) (2 + 2 * Math.tan(Math.PI / 2 / getMeasuredHeight() * (pullDownY + Math.abs(pullUpY)))); requestLayout(); if (pullDownY > 0) { if (pullDownY <= refreshDist && (state == STATE_RELEASE_TO_REFRESH || state == STATE_DONE)) { // 如果下拉距离没达到刷新的距离且当前状态是释放刷新,改变状态为下拉刷新 changeState(STATAE_INIT); } if (pullDownY >= refreshDist && state == STATAE_INIT) { // 如果下拉距离达到刷新的距离且当前状态是初始状态刷新,改变状态为释放刷新 changeState(STATE_RELEASE_TO_REFRESH); } } else if (pullUpY < 0) { // 下面是判断上拉加载的,同上,注意pullUpY是负值 if (-pullUpY <= loadMoreDist && (state == STATE_RELEASE_TO_LOAD || state == STATE_DONE)) { changeState(STATAE_INIT); } // 上拉操作 if (-pullUpY >= loadMoreDist && state == STATAE_INIT) { changeState(STATE_RELEASE_TO_LOAD); } } // 因为刷新和加载操作不能同时进行,所以pullDownY和pullUpY不会同时不为0,因此这里用(pullDownY + // Math.abs(pullUpY))就可以不对当前状态作区分了 if ((pullDownY + Math.abs(pullUpY)) > 8) { // 防止下拉过程中误触发长按事件和点击事件 ev.setAction(MotionEvent.ACTION_CANCEL); } break; case MotionEvent.ACTION_UP: if (pullDownY > refreshDist || -pullUpY > loadMoreDist) // 正在刷新时往下拉(正在加载时往上拉),释放后下拉头(上拉头)不隐藏 isTouch = false; if (state == STATE_RELEASE_TO_REFRESH) { changeState(STATE_REFRESHING); // 刷新操作 if (mListener != null) mListener.onRefresh(this); } else if (state == STATE_RELEASE_TO_LOAD) { changeState(STATE_LOADING); // 加载操作 if (mListener != null) mListener.onLoadMore(this); } hide(); default: break; } // 事件分发交给父类 super.dispatchTouchEvent(ev); return true; } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (!isLayout) { // 这里是第一次进来的时候做一些初始化 ensureHeadFoot(); isLayout = true; refreshDist = ((ViewGroup) refreshHeadView).getChildAt(0).getMeasuredHeight(); loadMoreDist = ((ViewGroup) loadmoreFootView).getChildAt(0).getMeasuredHeight(); } // 改变子控件的布局,这里直接用(pullDownY + pullUpY)作为偏移量,这样就可以不对当前状态作区分 refreshHeadView.layout(0, (int) (pullDownY + pullUpY) - refreshHeadView.getMeasuredHeight(), refreshHeadView.getMeasuredWidth(), (int) (pullDownY + pullUpY)); pullView.layout(0, (int) (pullDownY + pullUpY), pullView.getMeasuredWidth(), (int) (pullDownY + pullUpY) + pullView.getMeasuredHeight()); loadmoreFootView.layout(0, (int) (pullDownY + pullUpY) + pullView.getMeasuredHeight(), loadmoreFootView.getMeasuredWidth(), (int) (pullDownY + pullUpY) + pullView.getMeasuredHeight() + loadmoreFootView.getMeasuredHeight()); } @SuppressWarnings({"SimplifiableIfStatement", "RedundantIfStatement"}) public static class PullPolicy { public boolean canPullDown(View v) { if (v == null) { return false; } if (v instanceof AdapterView<?>) { return canPullDown((AdapterView<?>) v); } if (v instanceof ScrollView) { return canPullDown((ScrollView) v); } if (v instanceof WebView) { return canPullDown((WebView) v); } return true; } public boolean canPullUp(View v) { if (v == null) { return false; } if (v instanceof AdapterView<?>) { return canPullUp((AdapterView<?>) v); } if (v instanceof ScrollView) { return canPullUp((ScrollView) v); } if (v instanceof WebView) { return canPullUp((WebView) v); } return true; } protected boolean canPullDown(AdapterView<?> v) { if (v == null) { return false; } if (v.getCount() == 0) { // 没有item的时候也可以下拉刷新 return true; } else if (v.getFirstVisiblePosition() == 0 && v.getChildAt(0).getTop() >= 0) { // 滑到顶部了 return true; } else return false; } protected boolean canPullUp(AdapterView<?> v) { if (v == null) { return false; } if (v.getCount() == 0) { // 没有item的时候也可以上拉加载 return true; } else if (v.getLastVisiblePosition() == (v.getCount() - 1)) { // 滑到底部了 if (v.getChildAt(v.getLastVisiblePosition() - v.getFirstVisiblePosition()) != null && v.getChildAt(v.getLastVisiblePosition() - v.getFirstVisiblePosition()).getBottom() <= v .getMeasuredHeight()) return true; } return false; } protected boolean canPullDown(ScrollView v) { if (v == null) { return false; } if (v.getScrollY() == 0) return true; else return false; } protected boolean canPullUp(ScrollView v) { if (v == null) { return false; } if (v.getScrollY() >= (v.getChildAt(0).getHeight() - v.getMeasuredHeight())) return true; else return false; } protected boolean canPullDown(WebView v) { if (v == null) { return false; } if (v.getScrollY() == 0) return true; else return false; } protected boolean canPullUp(WebView v) { if (v == null) { return false; } if (v.getScrollY() >= Math.floor(v.getContentHeight() * v.getScale() - v.getMeasuredHeight())) return true; else return false; } } class MyTimer { private Handler handler; private Timer timer; private MyTask mTask; public MyTimer(Handler handler) { this.handler = handler; timer = new Timer(); } public void schedule(long period) { if (mTask != null) { mTask.cancel(); mTask = null; } mTask = new MyTask(handler); timer.schedule(mTask, 0, period); } public void cancel() { if (mTask != null) { mTask.cancel(); mTask = null; } } class MyTask extends TimerTask { private Handler handler; public MyTask(Handler handler) { this.handler = handler; } @Override public void run() { handler.obtainMessage().sendToTarget(); } } } }