新建PullToRefreshLayout继承LinearLayout
-
public class PullToRefreshLayout extends LinearLayout { private static final int STATUS_NORMAL = 1;//普通状态 private static final int STATUS_PULLING = 2;//正在上/下拉状态 private static final int STATUS_RELEASE = 3;//释放可刷新 private static final int STATUS_REFRESHING = 4;//正在刷新 private int currentStatus = STATUS_NORMAL;//当前状态记录 //头部布局及子View private View header; private ImageView headerIcon; private TextView headerInfo; //头高度 private int headerHeight; //头部刷新监听 private OnRefreshListener onRefreshListener; //中间的被刷新View private ViewGroup refresher; //底部布局及子布局 private View footer; private ImageView footerIcon; private TextView footerInfo; //底部高度 private int footerHeight; //底部刷新监听 private OnLoadListener onLoadListener; //处理滚动效果 private Scroller scroller; //旋转动画 private Animation rotateAnim; //按下时纵坐标,纵向滑动距离(绝对值), refresh高度 ,刷新类型(无刷新,仅上拉,仅下拉,全部) private int startY, offsetY, measuredHeight, type; public PullToRefreshLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); setOrientation(VERTICAL); //拿到自定义属性值 TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.PullToRefreshLayout); type = array.getInt(R.styleable.PullToRefreshLayout_pull_to_refresh_type, 1); array.recycle();//千万记得释放掉 //实例化控件 scroller = new Scroller(context); rotateAnim = AnimationUtils.loadAnimation(context, R.anim.common_loading); header = inflate(context, R.layout.common_pull_to_refresh, null); headerIcon = header.findViewById(R.id.common_pull_to_refresh_icon); headerInfo = header.findViewById(R.id.common_pull_to_refresh_info); footer = inflate(context, R.layout.common_pull_to_load, null); footerIcon = footer.findViewById(R.id.common_pull_to_load_icon); footerInfo = footer.findViewById(R.id.common_pull_to_load_info); } @Override protected void onFinishInflate() { super.onFinishInflate(); addView(header, 0);//添加头布局 refresher = (ViewGroup) getChildAt(1);//获取刷新布局 addView(footer, getChildCount());//添加底部布局 if (getChildCount() > 3) { throw new IllegalArgumentException("PullToRefreshLayout只能包含一个子View"); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); for (int i = 0; i < getChildCount(); i++) { measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //super.onLayout(changed,l,t,r,b);刷新的时候会卡 //获取测量的高度 headerHeight = headerHeight == 0 ? header.getMeasuredHeight() : headerHeight; footerHeight = footerHeight == 0 ? footer.getMeasuredHeight() : footerHeight; measuredHeight = measuredHeight == 0 ? getMeasuredHeight() : measuredHeight; //将头文件向上隐藏 header.layout(l, -headerHeight, r, 0); refresher.layout(l, 0, r, measuredHeight); //将底部文件向下隐藏 footer.layout(l, measuredHeight, r, measuredHeight + footerHeight); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: startY = (int) ev.getRawY(); break; case MotionEvent.ACTION_MOVE: if (type == 1) {//无刷新状态返回false,不处理touch事件 return false; } if (currentStatus == STATUS_REFRESHING) {//当前正在刷新,屏蔽掉touch事件 return true; } if ((ev.getRawY() > startY && isViewGroupTopper()) || (ev.getRawY() < startY && isViewGroupFooter())) {//向下滑动且refresher在最顶部||向上滑动且refresher在最底部 return true; } break; case MotionEvent.ACTION_UP: if (currentStatus != STATUS_NORMAL) {//手指抬起时不是正常状态 return true; } break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_MOVE: if (currentStatus == STATUS_REFRESHING) {//正在刷新直接return掉 return true; } offsetY = Math.abs((int) ((event.getRawY() - startY) * 0.3));//计算纵向偏移 阻尼0.3f if (event.getRawY() > startY && isViewGroupTopper() && (type == 2 || type == 4)) {//向下滑且refresher在最顶部且类型为仅下拉或全部 updateHeaderStatus(offsetY < headerHeight ? STATUS_PULLING : STATUS_RELEASE); offsetY = offsetY < headerHeight ? offsetY : headerHeight; scrollTo(getScrollX(), -offsetY); } else if (event.getRawY() < startY && isViewGroupFooter() && (type == 3 || type == 4)) {//向上滑且refresher在最底部且类型为仅上拉或全部 updateFooterStatus(offsetY < footerHeight ? STATUS_PULLING : STATUS_RELEASE); offsetY = offsetY < footerHeight ? offsetY : footerHeight; scrollTo(getScrollX(), offsetY); } break; case MotionEvent.ACTION_UP: if (event.getRawY() > startY && isViewGroupTopper() && (type == 2 || type == 4)) { if (currentStatus == STATUS_PULLING) {//状态是正在拉动就松手 回滚布局 updateHeaderStatus(STATUS_NORMAL); scroller.startScroll(getScrollX(), getScrollY(), 0, offsetY, 600); postInvalidate(); } else if (currentStatus == STATUS_RELEASE) {//状态是可刷新 updateHeaderStatus(STATUS_REFRESHING); offsetY = headerHeight; } } else if (event.getRawY() < startY && isViewGroupFooter() && (type == 3 || type == 4)) { if (currentStatus == STATUS_PULLING) { updateFooterStatus(STATUS_NORMAL); scroller.startScroll(getScrollX(), getScrollY(), 0, -offsetY, 600); postInvalidate(); } else if (currentStatus == STATUS_RELEASE) { updateFooterStatus(STATUS_REFRESHING); offsetY = footerHeight; } } break; } return true; } //重写computeScroll @Override public void computeScroll() { if (scroller.computeScrollOffset()) { ((View) refresher.getParent()).scrollTo(scroller.getCurrX(), scroller.getCurrY()); postInvalidate(); } super.computeScroll(); } private void updateHeaderStatus(int status) { currentStatus = status; if (currentStatus == STATUS_NORMAL) { headerIcon.clearAnimation(); } else if (currentStatus == STATUS_PULLING) { headerInfo.setText("下拉可以刷新"); header.clearAnimation(); } else if (currentStatus == STATUS_RELEASE) { headerInfo.setText("松开可以刷新"); } else if (currentStatus == STATUS_REFRESHING) { headerInfo.setText("正在努力刷新"); headerIcon.startAnimation(rotateAnim); if (onRefreshListener != null) { onRefreshListener.onRefresh(); } } } private void updateFooterStatus(int status) { currentStatus = status; if (currentStatus == STATUS_NORMAL) { footerIcon.clearAnimation(); } else if (currentStatus == STATUS_PULLING) { footerIcon.clearAnimation(); footerInfo.setText("上拉加载更多"); } else if (currentStatus == STATUS_RELEASE) { footerInfo.setText("松开加载更多"); } else if (currentStatus == STATUS_REFRESHING) { footerInfo.setText("正在努力加载"); footerIcon.startAnimation(rotateAnim); if (onLoadListener != null) { onLoadListener.onLoad(); } } } //判断refresher是否在最顶部 public boolean isViewGroupTopper() { return !refresher.canScrollVertically(-1); } //判断refresher是否在最底部 private boolean isViewGroupFooter() { return !refresher.canScrollVertically(1); } public void completeRefresh() { headerInfo.setText("刷新成功\u3000\u3000"); handler.sendEmptyMessageDelayed(0, 500); } public void completeLoad() { footerInfo.setText("加载成功\u3000\u3000"); handler.sendEmptyMessageDelayed(1, 500); } public void setOnRefreshListener(OnRefreshListener onRefreshListener) { this.onRefreshListener = onRefreshListener; } public void setOnLoadListener(OnLoadListener onLoadListener) { this.onLoadListener = onLoadListener; } public interface OnRefreshListener { void onRefresh(); } public interface OnLoadListener { void onLoad(); } public ViewGroup getRefresher() { return refresher; } private Handler handler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { if (msg.what == 0) { scroller.startScroll(getScrollX(), getScrollY(), 0, offsetY, 500); updateHeaderStatus(STATUS_NORMAL); } else { scroller.startScroll(getScrollX(), getScrollY(), 0, -offsetY, 500); updateFooterStatus(STATUS_NORMAL); } postInvalidate(); return true; } }); }
-
style文件
<declare-styleable name="PullToRefreshLayout" tools:ignore="ResourceName"> <attr name="pull_to_refresh_type" format="enum"> <enum name="none" value="1" /> <enum name="refresh" value="2" /> <enum name="load" value="3" /> <enum name="all" value="4" /> </attr> </declare-styleable>
-
旋转动画
<?xml version="1.0" encoding="utf-8"?> <rotate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="1000" android:fromDegrees="0" android:interpolator="@android:anim/linear_interpolator" android:pivotX="50%" android:pivotY="50%" android:repeatCount="-1" android:toDegrees="360" />
-
布局文件由于公司的原因就不往外放了,很简单 ,使用的话就像ScrollView一样只能有一个子布局
-
感谢大牛 郭霖的博客启发