自定义下拉刷新和上拉加载框架

Tags
Star
#Android
看过很多的下拉刷新框架,但感觉大多是基于ListView或RecyclerView。个人觉得,下拉上拉做为一个通用操作,最好是做为一个专门的容器,和视图展示分离开来,这样就算内容展示视图要从ListView变成RecyclerViewl了,下拉上拉这一层,也无需做任何改动!

1.整体思路

  1. 自定义测量以及布局的方法
  1. 拦截掉子控件的一些手势
  1. 处理手势,刷新状态
结构图.png

2.自定义测量和布局

继承ViewGroup,重写onMeasure与onLayout方法,这里要注意当子控件GONE的情况
public abstract class DrawLayout extends ViewGroup {

public View header;
public View footer;

public PullHeader pullHeader;
public PullFooter pullFooter;

public int bottomScroll;// 当滚动到内容最底部时Y轴所需要的滑动值
public int lastChildIndex;// 最后一个childview的index

public DrawLayout(Context context) {
    super(context);
}

public DrawLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
}

public void setHeader(PullHeader pullHeader) {
    this.pullHeader = pullHeader;
}

public void setFooter(PullFooter pullFooter) {
    this.pullFooter = pullFooter;
}

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    lastChildIndex = getChildCount() 1;
}

/**
 * 添加上拉刷新布局作为header
 */
public void addHeader(View header) {
    this.header = header;
    ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    addView(header, layoutParams);
}

/**
 * 添加下拉加载布局作为footer
 */
public void addFooter(View footer) {
    this.footer = footer;
    ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    addView(footer, layoutParams);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 遍历进行子视图的测量工作
    for (int i = 0; i < getChildCount(); i++) {
        // 通知子视图进行测量
        View child = getChildAt(i);
        if (child.getVisibility() == GONE) {
            continue;
        }
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
    }
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    // 重置(避免重复累加)
    int contentHeight = 0;

    // 遍历进行子视图的置位工作
    for (int index = 0; index < getChildCount(); index++) {
        View child = getChildAt(index);
        if (child.getVisibility() == GONE) {
            continue;
        }
        // 头视图隐藏在ViewGroup的顶端
        if (child == header) {
       
这里,我定义了两个接口: 下拉接口:
public interface PullHeader {

//下拉刷新(下拉中,到达有效刷新距离前)
void onDownBefore(int scrollY);

//松开刷新(下拉中,到达有效刷新距离后)
void onDownAfter(int scrollY);

//准备刷新(从松手后的位置滚动到刷新的位置)
void onRefreshScrolling(int scrollY);

//正在刷新……
void onRefreshDoing(int scrollY);

//刷新完成后,回到默认状态中
void onRefreshCompleteScrolling(int scrollY, boolean isRefreshSuccess);

//刷新取消后,回到默认状态中(没有超过有效的下拉距离)
void onRefreshCancelScrolling(int scrollY);
}
上拉接口:
public interface PullFooter {

//上拉加载
void onUpBefore(int scrollY);

//松开加载
void onUpAfter(int scrollY);

//准备加载
void onLoadScrolling(int scrollY);

//正在加载……
void onLoadDoing(int scrollY);

//加载完成后,回到默认状态中
void onLoadCompleteScrolling(int scrollY, boolean isLoadSuccess);

//加载取消后,回到默认状态中
void onLoadCancelScrolling(int scrollY);
}
大家可以看到,我这里每一个回调中,都返回了scrollY,方便我们根据该值做一些自定义的动画效果。

3.自定义拦截手势

继承刚才的DrawLayout,重写onInterceptTouchEvent方法,这里主要是要解决当子控件也可以滑动时的一些冲突问题。
比如当子控件是ScrollView时,只有当ScrollView滑动到顶部或底部,不能再滑动时,才可以触发下拉或上拉事件。
public abstract class InterceptLauyout extends DrawLayout {

// 用于计算滑动距离的Y坐标中介
public int lastYMove;
// 用于判断是否拦截触摸事件的Y坐标中介
public int lastYIntercept;

public InterceptLauyout(Context context) {
    super(context);
}

public InterceptLauyout(Context context, AttributeSet attrs) {
    super(context, attrs);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercept = false;
    // 记录此次触摸事件的y坐标
    int y = (int) event.getY();
    // 判断触摸事件类型
    switch (event.getAction()) {
        // Down事件
        case MotionEvent.ACTION_DOWN: {
            // 记录下本次系列触摸事件的起始点Y坐标
            lastYMove = y;
            // 不拦截ACTION_DOWN,因为当ACTION_DOWN被拦截,后续所有触摸事件都会被拦截
            intercept = false;
            break;
        }
        // Move事件
        case MotionEvent.ACTION_MOVE: {
            if (y > lastYIntercept) { // 下滑操作
                // 获取最顶部的子视图
                View child = getFirstVisiableChild();
                if (child == null) {
                    intercept = false;
                } else if (child instanceof AdapterView) {
                    intercept = avPullDownIntercept(child);
                } else if (child instanceof ScrollView) {
                    intercept = svPullDownIntercept(child);
                } else if (child instanceof RecyclerView) {
                    intercept = rvPullDownIntercept(child);
                }
            } else if (y < lastYIntercept) { // 上拉操作
                // 获取最底部的子视图
                View child = getLastVisiableChild();
                if (child == null) {
                    intercept = false;
                } else if (child instanceof AdapterView) {
                    intercept = avPullUpIntercept(child);
                } else if (child instanceof ScrollView) {
                    intercept = svPullUpIntercept(child);
                } else if (child instanceof RecyclerView) {
                    intercept = rvPullUpIntercept(child);
                }
            } else {
          

4.自定义处理手势,刷新状态

首先,我定义了下拉的所有状态,基本上这里的每一种状态都对应着上面的一种接口回调。
public enum PullStatus {
DEFAULT,//默认状态

DOWN_BEFORE,//下拉中,到达有效刷新距离前
DOWN_AFTER,//下拉中,到达有效刷新距离后
REFRESH_SCROLLING,//放手后,开始刷新前,回到刷新的位置中
REFRESH_DOING,//正在刷新中
REFRESH_COMPLETE_SCROLLING,//刷新完成后,回到默认状态中
REFRESH_CANCEL_SCROLLING,//刷新取消后,回到默认状态中

UP_BEFORE,//上拉中,到达有效刷新距离前
UP_AFTER,//上拉中,到达有效刷新距离后
LOADMORE_SCROLLING,//放手后,开始加载前,从手势位置回到加载的位置中
LOADMORE_DOING,//正在加载中
LOADMORE_COMPLETE_SCROLLING,//加载完成后,回到默认状态中
LOADMORE_CANCEL_SCROLLING,//加载取消后,回到默认状态中

}
接着,继承刚才的InterceptLauyout,重写onTouchEvent方法,刷新状态。这里我主要是通过属性动画+scrollTo/scrollBy来实现弹性滑动的。当然你也可以用scroller来实现。 需要注意scrollY和我们的视图坐标系方向相反!
public class PullLayout extends InterceptLauyout {
// 事件监听接口
private OnPullListener listener;
// Layout状态
private PullStatus status = PullStatus.DEFAULT;
//阻尼系数
private float damp = 0.5f;
//恢复动画的执行时间
public int SCROLL_TIME = 300;
//是否刷新完成
private boolean isRefreshSuccess = false;
//是否加载完成
private boolean isLoadSuccess = false;

public void setOnPullListener(OnPullListener listener) {
    this.listener = listener;
}

public PullLayout(Context context) {
    super(context);
}

public PullLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE: {
            // 计算本次滑动的Y轴增量(距离)
            int dy = y lastYMove;
            LogUtil.print("dy=" + dy + "\tgetScrollY=" + getScrollY());
            // 如果getScrollY<0,即下拉操作
            if (getScrollY() < 0) {
                if (header != null) {
                    // 进行Y轴上的滑动
                    performScroll(dy);
                    if (Math.abs(getScrollY()) > header.getMeasuredHeight()) {
                        updateStatus(DOWN_AFTER);
                    } else {
                        updateStatus(DOWN_BEFORE);
                    }
                }
            }
            // 如果getScrollY>=0,即上拉操作
            else {
                if (footer != null) {
                    // 进行Y轴上的滑动
                    performScroll(dy);
                    if (getScrollY() >= bottomScroll + footer.getMeasuredHeight()) {
                        updateStatus(UP_AFTER);
                    } else {
                        updateStatus(UP_BEFORE);
                    }
                }
            }
            // 记录y坐标
            lastYMove = y;
            break;
        }

        case MotionEvent.ACTION_UP: {
            // 判断本次触摸系列事件结束时,Layout的状态
            switch (status) {
                //下拉刷新
                case DOWN_BEFORE:
                
这里有一个供外部调用的监听器:
public interface OnPullListener {

//执行刷新
void onRefresh();

//执行加载更多
void onLoadMore();
}

5.在项目中使用时的基本配置

通过上面的代码大家可以看到,我的下拉刷新框架,没有依赖任何res资源,很方便大家copy,或者直接依赖jar。
一般整个应用会有一个统一的下拉刷新效果和上拉加载效果,所以,我们可以再自定义一个应用的刷新布局,继承自PullLayout。
public class RefreshLayout extends PullLayout {
public RefreshLayout(Context context) {
    super(context);
}

public RefreshLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
}

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    init();
}

public void init() {
    HeaderView header = new HeaderView(getContext());
    FooterView footer = new FooterView(getContext());

    addHeader(header);
    addFooter(footer);

    setHeader(header);
    setFooter(footer);
}

}
这里的HeaderView和FooterView分别实现了PullHeader和PullFooter接口。
public class HeaderView extends FrameLayout implements PullHeader {

public TextView tvPullDown;

public HeaderView(Context context) {
    super(context);
    LayoutInflater.from(context).inflate(R.layout.layout_header, this, true);
    tvPullDown = (TextView) findViewById(R.id.tv);
}

@Override
public void onDownBefore(int scrollY) {
    tvPullDown.setText("下拉刷新");
}

@Override
public void onDownAfter(int scrollY) {
    tvPullDown.setText("松开刷新");
}

@Override
public void onRefreshScrolling(int scrollY) {
    tvPullDown.setText("准备刷新");
}

@Override
public void onRefreshDoing(int scrollY) {
    tvPullDown.setText("正在刷新……");
}

@Override
public void onRefreshCompleteScrolling(int scrollY, boolean isLoadSuccess) {
    tvPullDown.setText(isLoadSuccess ? "刷新成功" : "刷新失败");
}

@Override
public void onRefreshCancelScrolling(int scrollY) {
    tvPullDown.setText("取消刷新");
}
}
public class FooterView extends FrameLayout implements PullFooter {

public TextView tvPullUp;

public FooterView(Context context) {
    super(context);
    LayoutInflater.from(context).inflate(R.layout.layout_footer, this, true);
    tvPullUp = (TextView) findViewById(R.id.tv);
}


@Override
public void onUpBefore(int scrollY) {
    tvPullUp.setText("上拉加载更多");
}

@Override
public void onUpAfter(int scrollY) {
    tvPullUp.setText("松开加载更多");
}

@Override
public void onLoadScrolling(int scrollY) {
    tvPullUp.setText("准备加载");
}

@Override
public void onLoadDoing(int scrollY) {
    tvPullUp.setText("正在加载……");
}

@Override
public void onLoadCompleteScrolling(int scrollY, boolean isLoadSuccess) {
    tvPullUp.setText(isLoadSuccess ? "加载成功" : "加载失败");
}

@Override
public void onLoadCancelScrolling(int scrollY) {
    tvPullUp.setText("加载取消");
}
}
它们的布局资源也是很简单的: layout_header:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#f5f5f5">

<TextView
    android:id="@+id/tv"
    android:layout_width="match_parent"
    android:layout_height="@dimen/header_height"
    android:gravity="center"
    android:text="下拉刷新"
    android:textColor="#000000"
    android:textSize="16sp"/>

</RelativeLayout>
layout_footer:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#f5f5f5">

<TextView
    android:id="@+id/tv"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="@dimen/header_height"
    android:text="上拉加载更多"
    android:textColor="#000000"
    android:textSize="16sp"/>

</RelativeLayout>

6.加个辅助类,让集成变得更加简单

本来呢,写到第5点,其实就不用写了,不过,考虑到有时候,一个应用中多个界面都有下拉刷新时,经常会有许多类似的代码块,所以,我又定义了一个辅助类。
这里的SingleAdapter、SuperViewHolder是使用的我的另一个库:fast-adapter:Adapter的封装之路
而IWebLoading、WebTransformer、WebSubscriber等呢,则是与我的另一个库:fast-http有关,这是一个Retrofit+OkHttp+RxJava的封装库,暂时没写博客仔细整理,以后再分享,大家可以不用管它,那个与本节无关,IWebLoading是为了让初始数据时有loading,刷新或加载更多时没有loading。WebTransformer是为了通用的线程切换。WebSubscriber是为了通用的错误处理。大家可以先看看我的这两篇:Retrofit基本用法和流程分析Okhttp基本用法和流程分析
public class RefreshHelper<T> {

public RefreshLayout viewRefresh;
public RefreshInterface<T> refreshInterface;
public int layoutId;

public Context context;
public IWebLoading webLoading;
public View viewEmpty;
public RecyclerView rv;
public List<T> data;

public SingleAdapter<T> adapter;
public int curPage = 1;//当前页码
public boolean isRefresh = false;//是否正在刷新
public boolean isLoadMore = false;//是否正在加载更多

public RefreshHelper(RefreshLayout viewRefresh,RefreshInterface<T> refreshInterface,int layoutId) {
    this.viewRefresh=viewRefresh;
    this.refreshInterface=refreshInterface;
    this.layoutId=layoutId;

    context=viewRefresh.getContext();
    webLoading=new LoadingDialog(context);
    viewEmpty=viewRefresh.getChildAt(0);
    rv= (RecyclerView) viewRefresh.getChildAt(1);
    data = new ArrayList<>();

    initRv();
    initRefresh();
    loadData();
}

private void initRv() {
    LinearLayoutManager layoutManager = new LinearLayoutManager(context);
    rv.setLayoutManager(layoutManager);
    adapter = new SingleAdapter<T>(context, layoutId) {
        @Override
        protected void bindData(SuperViewHolder holder, T item) {
            refreshInterface.bindData(holder, item);
        }
    };
    rv.setAdapter(adapter);
}

private void initRefresh() {
    viewRefresh.setOnPullListener(new OnPullListener() {
        @Override
        public void onRefresh() {
            LogUtil.print("");
            isRefresh = true;
            curPage = 1;
            data = new ArrayList<>();
            loadData();
        }

        @Override
        public void onLoadMore() {
            LogUtil.print("");
            isLoadMore = true;
            curPage++;
            loadData();
        }
    });
}

private void loadData() {
    if (isRefresh || isLoadMore) {
        webLoading = null;
    }
    refreshInterface.getData(curPage)
            .compose(new WebTransformer<>(webLoading))
            .subscribe(new WebSubscriber<List<T>>(webLoading) {
                @Ov
这里注意,RefreshLayout里面的子控件必须是这样的,第一个是viewEmpty,第二个是recyclerView(当然,你要是其它的,也可以,改辅助类吧,我这个辅助类只针对recyclerView):
<com.che.lovecar.support.view.pull.RefreshLayout
    android:id="@+id/view_refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:id="@+id/view_empty"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        android:visibility="gone">

        <ImageView
            android:layout_width="150dp"
            android:layout_height="150dp"
            android:scaleType="centerInside"
            android:src="@drawable/icon_nomessage"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="暂无消息"
            android:textColor="@color/text_d"
            android:textSize="20sp"/>

    </LinearLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_msg"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:overScrollMode="never"
        android:visibility="visible"/>

</com.che.lovecar.support.view.pull.RefreshLayout>
然后,在Activity或Fragment中使用时,我只需要这样: 1.添加一个RefreshHelper
private RefreshHelper<Message> refreshHelper;

refreshHelper = new RefreshHelper<Message>(viewRefresh, this, R.layout.item_msg);
2.Activity实现RefreshInterface接口,实现getData和bindData方法
public class MsgListActivity extends BaseActivity implements RefreshHelper.RefreshInterface<Message>
@Override
public void bindData(SuperViewHolder holder, Message item) {
    View rootView = holder.getRootView();
    View dot = holder.getView(R.id.dot_msg);
    TextView tvTime = holder.getView(R.id.tv_time);
    TextView tvMsg = holder.getView(R.id.tv_msg);

    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");

    dot.setVisibility(item.getRead() == 0 ? View.VISIBLE : View.GONE);
    tvTime.setText(dateFormat.format(item.getCreate_time()));
    tvMsg.setText(item.getContent());
    rootView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            readMsg(item, dot);
        }
    });
}

@Override
public Observable<List<Message>> getData(int curPage) {
    return Observable.create(subscriber -> {
        if (subscriber.isUnsubscribed()) return;
        try {
            Thread.sleep(3000);
            List<Message> list = new ArrayList<>();
            if (!isEmpty) {
                LogUtil.print("加载本地的json");
                String json = FileUtil.getFromAssets(getActivity(), "json/list.json");
                MessageListResponse response = JSON.parseObject(json, MessageListResponse.class);
                list = response.getMessageListPojoList();
            }
            subscriber.onNext(list);
            subscriber.onCompleted();
        } catch (Exception e) {
            subscriber.onError(e);
        }
    });
}

参考目录

  1. 自个儿写Android的下拉刷新/上拉加载控件
  1. android-Ultra-Pull-To-Refresh 源码解析
  1. Android下拉刷新完全解析,教你如何一分钟实现下拉刷新功能

© fishyer 2022